├── .gitignore ├── Data └── UserBehaviour.txt ├── Example ├── App.config ├── Example.csproj ├── MainForm.Designer.cs ├── MainForm.cs ├── MainForm.resx ├── Program.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── RecommenderTests.cs └── icon.ico ├── LICENSE ├── README.md ├── User Behavior.sln └── User Behavior ├── Abstractions ├── IClassifier.cs ├── IComparer.cs ├── IRater.cs └── ISplitter.cs ├── Comparers ├── CoRatedCosineUserComparer.cs ├── CorrelationUserComparer.cs ├── CosineUserComparer.cs ├── RootMeanSquareUserComparer.cs └── SimpleCountUserComparer.cs ├── Mathematics ├── Matrix.cs ├── SingularValueDecomposition.cs └── SvdResult.cs ├── Objects ├── Article.cs ├── ArticleRating.cs ├── ArticleTag.cs ├── ArticleTagCounts.cs ├── ScoreResults.cs ├── Suggestion.cs ├── Tag.cs ├── TestResults.cs ├── User.cs ├── UserAction.cs ├── UserActionTag.cs ├── UserArticleRatings.cs └── UserArticleRatingsTable.cs ├── Parsers ├── DaySplitter.cs ├── UserBehaviorDatabase.cs ├── UserBehaviorDatabaseParser.cs └── UserBehaviorTransformer.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs ├── Resources.resx ├── Settings.Designer.cs └── Settings.settings ├── Raters ├── LinearRater.cs ├── SimpleRater.cs └── WeightedRater.cs ├── Recommender.csproj └── Recommenders ├── ClassifierExtensions.cs ├── HybridRecommender.cs ├── ItemCollaborativeFilterRecommender.cs ├── MatrixFactorizationRecommender.cs ├── RandomRecommender.cs └── UserCollaborativeFilterRecommender.cs /.gitignore: -------------------------------------------------------------------------------- 1 | *.cache 2 | *.suo 3 | *.manifest 4 | *.key 5 | bin/ 6 | obj/ 7 | packages/ 8 | *.vspx 9 | *.diagsession -------------------------------------------------------------------------------- /Example/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Example/Example.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {A27FF3EC-EE8E-4EE8-B6F2-60572619CEC7} 8 | WinExe 9 | Properties 10 | Example 11 | RecommenderExample 12 | v4.5.2 13 | 512 14 | true 15 | 16 | 17 | x64 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | x64 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | icon.ico 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | Form 54 | 55 | 56 | MainForm.cs 57 | 58 | 59 | 60 | 61 | 62 | MainForm.cs 63 | 64 | 65 | ResXFileCodeGenerator 66 | Resources.Designer.cs 67 | Designer 68 | 69 | 70 | True 71 | Resources.resx 72 | 73 | 74 | SettingsSingleFileGenerator 75 | Settings.Designer.cs 76 | 77 | 78 | True 79 | Settings.settings 80 | True 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | {db5a3fef-d18f-4901-9b4f-959e403bd21b} 89 | Recommender 90 | 91 | 92 | 93 | 94 | 95 | 96 | 103 | -------------------------------------------------------------------------------- /Example/MainForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace Example 2 | { 3 | partial class MainForm 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); 32 | this.ofdGetUserBehaviors = new System.Windows.Forms.OpenFileDialog(); 33 | this.bgWorker = new System.ComponentModel.BackgroundWorker(); 34 | this.groupTrain = new System.Windows.Forms.GroupBox(); 35 | this.btnLoadTrain = new System.Windows.Forms.Button(); 36 | this.groupScore = new System.Windows.Forms.GroupBox(); 37 | this.txtScoreArticle = new System.Windows.Forms.TextBox(); 38 | this.label2 = new System.Windows.Forms.Label(); 39 | this.txtScoreUser = new System.Windows.Forms.TextBox(); 40 | this.label1 = new System.Windows.Forms.Label(); 41 | this.btnScore = new System.Windows.Forms.Button(); 42 | this.groupRecommend = new System.Windows.Forms.GroupBox(); 43 | this.txtRecommendNum = new System.Windows.Forms.TextBox(); 44 | this.label3 = new System.Windows.Forms.Label(); 45 | this.txtRecommendUser = new System.Windows.Forms.TextBox(); 46 | this.label4 = new System.Windows.Forms.Label(); 47 | this.btnRecommend = new System.Windows.Forms.Button(); 48 | this.rtbOutput = new System.Windows.Forms.RichTextBox(); 49 | this.bgScore = new System.ComponentModel.BackgroundWorker(); 50 | this.bgRecommend = new System.ComponentModel.BackgroundWorker(); 51 | this.groupTrain.SuspendLayout(); 52 | this.groupScore.SuspendLayout(); 53 | this.groupRecommend.SuspendLayout(); 54 | this.SuspendLayout(); 55 | // 56 | // ofdGetUserBehaviors 57 | // 58 | this.ofdGetUserBehaviors.FileName = "UserBehaviour.txt"; 59 | // 60 | // bgWorker 61 | // 62 | this.bgWorker.DoWork += new System.ComponentModel.DoWorkEventHandler(this.bgWorker_DoWork); 63 | this.bgWorker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.bgWorker_RunWorkerCompleted); 64 | // 65 | // groupTrain 66 | // 67 | this.groupTrain.Controls.Add(this.btnLoadTrain); 68 | this.groupTrain.Location = new System.Drawing.Point(12, 12); 69 | this.groupTrain.Name = "groupTrain"; 70 | this.groupTrain.Size = new System.Drawing.Size(282, 49); 71 | this.groupTrain.TabIndex = 2; 72 | this.groupTrain.TabStop = false; 73 | this.groupTrain.Text = "Train the Classifier"; 74 | // 75 | // btnLoadTrain 76 | // 77 | this.btnLoadTrain.Location = new System.Drawing.Point(6, 19); 78 | this.btnLoadTrain.Name = "btnLoadTrain"; 79 | this.btnLoadTrain.Size = new System.Drawing.Size(270, 23); 80 | this.btnLoadTrain.TabIndex = 0; 81 | this.btnLoadTrain.Text = "Load Data and Train"; 82 | this.btnLoadTrain.UseVisualStyleBackColor = true; 83 | this.btnLoadTrain.Click += new System.EventHandler(this.btnLoadTrain_Click); 84 | // 85 | // groupScore 86 | // 87 | this.groupScore.Controls.Add(this.txtScoreArticle); 88 | this.groupScore.Controls.Add(this.label2); 89 | this.groupScore.Controls.Add(this.txtScoreUser); 90 | this.groupScore.Controls.Add(this.label1); 91 | this.groupScore.Controls.Add(this.btnScore); 92 | this.groupScore.Enabled = false; 93 | this.groupScore.Location = new System.Drawing.Point(12, 67); 94 | this.groupScore.Name = "groupScore"; 95 | this.groupScore.Size = new System.Drawing.Size(138, 101); 96 | this.groupScore.TabIndex = 2; 97 | this.groupScore.TabStop = false; 98 | this.groupScore.Text = "Score an Article"; 99 | // 100 | // txtScoreArticle 101 | // 102 | this.txtScoreArticle.Location = new System.Drawing.Point(67, 45); 103 | this.txtScoreArticle.Name = "txtScoreArticle"; 104 | this.txtScoreArticle.Size = new System.Drawing.Size(65, 20); 105 | this.txtScoreArticle.TabIndex = 4; 106 | // 107 | // label2 108 | // 109 | this.label2.AutoSize = true; 110 | this.label2.Location = new System.Drawing.Point(11, 49); 111 | this.label2.Name = "label2"; 112 | this.label2.Size = new System.Drawing.Size(50, 13); 113 | this.label2.TabIndex = 3; 114 | this.label2.Text = "Article ID"; 115 | // 116 | // txtScoreUser 117 | // 118 | this.txtScoreUser.Location = new System.Drawing.Point(67, 19); 119 | this.txtScoreUser.Name = "txtScoreUser"; 120 | this.txtScoreUser.Size = new System.Drawing.Size(65, 20); 121 | this.txtScoreUser.TabIndex = 2; 122 | // 123 | // label1 124 | // 125 | this.label1.AutoSize = true; 126 | this.label1.Location = new System.Drawing.Point(18, 23); 127 | this.label1.Name = "label1"; 128 | this.label1.Size = new System.Drawing.Size(43, 13); 129 | this.label1.TabIndex = 1; 130 | this.label1.Text = "User ID"; 131 | // 132 | // btnScore 133 | // 134 | this.btnScore.Location = new System.Drawing.Point(6, 71); 135 | this.btnScore.Name = "btnScore"; 136 | this.btnScore.Size = new System.Drawing.Size(126, 23); 137 | this.btnScore.TabIndex = 0; 138 | this.btnScore.Text = "Score"; 139 | this.btnScore.UseVisualStyleBackColor = true; 140 | this.btnScore.Click += new System.EventHandler(this.btnScore_Click); 141 | // 142 | // groupRecommend 143 | // 144 | this.groupRecommend.Controls.Add(this.txtRecommendNum); 145 | this.groupRecommend.Controls.Add(this.label3); 146 | this.groupRecommend.Controls.Add(this.txtRecommendUser); 147 | this.groupRecommend.Controls.Add(this.label4); 148 | this.groupRecommend.Controls.Add(this.btnRecommend); 149 | this.groupRecommend.Enabled = false; 150 | this.groupRecommend.Location = new System.Drawing.Point(156, 67); 151 | this.groupRecommend.Name = "groupRecommend"; 152 | this.groupRecommend.Size = new System.Drawing.Size(138, 101); 153 | this.groupRecommend.TabIndex = 2; 154 | this.groupRecommend.TabStop = false; 155 | this.groupRecommend.Text = "Recommend Articles"; 156 | // 157 | // txtRecommendNum 158 | // 159 | this.txtRecommendNum.Location = new System.Drawing.Point(67, 45); 160 | this.txtRecommendNum.Name = "txtRecommendNum"; 161 | this.txtRecommendNum.Size = new System.Drawing.Size(65, 20); 162 | this.txtRecommendNum.TabIndex = 4; 163 | // 164 | // label3 165 | // 166 | this.label3.AutoSize = true; 167 | this.label3.Location = new System.Drawing.Point(10, 49); 168 | this.label3.Name = "label3"; 169 | this.label3.Size = new System.Drawing.Size(51, 13); 170 | this.label3.TabIndex = 3; 171 | this.label3.Text = "# Articles"; 172 | // 173 | // txtRecommendUser 174 | // 175 | this.txtRecommendUser.Location = new System.Drawing.Point(67, 19); 176 | this.txtRecommendUser.Name = "txtRecommendUser"; 177 | this.txtRecommendUser.Size = new System.Drawing.Size(65, 20); 178 | this.txtRecommendUser.TabIndex = 2; 179 | // 180 | // label4 181 | // 182 | this.label4.AutoSize = true; 183 | this.label4.Location = new System.Drawing.Point(18, 23); 184 | this.label4.Name = "label4"; 185 | this.label4.Size = new System.Drawing.Size(43, 13); 186 | this.label4.TabIndex = 1; 187 | this.label4.Text = "User ID"; 188 | // 189 | // btnRecommend 190 | // 191 | this.btnRecommend.Location = new System.Drawing.Point(6, 71); 192 | this.btnRecommend.Name = "btnRecommend"; 193 | this.btnRecommend.Size = new System.Drawing.Size(126, 23); 194 | this.btnRecommend.TabIndex = 0; 195 | this.btnRecommend.Text = "Recommend"; 196 | this.btnRecommend.UseVisualStyleBackColor = true; 197 | this.btnRecommend.Click += new System.EventHandler(this.btnRecommend_Click); 198 | // 199 | // rtbOutput 200 | // 201 | this.rtbOutput.Font = new System.Drawing.Font("Consolas", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); 202 | this.rtbOutput.Location = new System.Drawing.Point(12, 174); 203 | this.rtbOutput.Name = "rtbOutput"; 204 | this.rtbOutput.ReadOnly = true; 205 | this.rtbOutput.Size = new System.Drawing.Size(282, 170); 206 | this.rtbOutput.TabIndex = 3; 207 | this.rtbOutput.Text = ""; 208 | // 209 | // bgScore 210 | // 211 | this.bgScore.DoWork += new System.ComponentModel.DoWorkEventHandler(this.bgScore_DoWork); 212 | this.bgScore.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.bgScore_RunWorkerCompleted); 213 | // 214 | // bgRecommend 215 | // 216 | this.bgRecommend.DoWork += new System.ComponentModel.DoWorkEventHandler(this.bgRecommend_DoWork); 217 | this.bgRecommend.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.bgRecommend_RunWorkerCompleted); 218 | // 219 | // MainForm 220 | // 221 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 222 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 223 | this.ClientSize = new System.Drawing.Size(306, 356); 224 | this.Controls.Add(this.rtbOutput); 225 | this.Controls.Add(this.groupRecommend); 226 | this.Controls.Add(this.groupScore); 227 | this.Controls.Add(this.groupTrain); 228 | this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); 229 | this.Name = "MainForm"; 230 | this.Text = "Recommender - Scott Clayton"; 231 | this.groupTrain.ResumeLayout(false); 232 | this.groupScore.ResumeLayout(false); 233 | this.groupScore.PerformLayout(); 234 | this.groupRecommend.ResumeLayout(false); 235 | this.groupRecommend.PerformLayout(); 236 | this.ResumeLayout(false); 237 | 238 | } 239 | 240 | #endregion 241 | private System.Windows.Forms.OpenFileDialog ofdGetUserBehaviors; 242 | private System.ComponentModel.BackgroundWorker bgWorker; 243 | private System.Windows.Forms.GroupBox groupTrain; 244 | private System.Windows.Forms.Button btnLoadTrain; 245 | private System.Windows.Forms.GroupBox groupScore; 246 | private System.Windows.Forms.TextBox txtScoreArticle; 247 | private System.Windows.Forms.Label label2; 248 | private System.Windows.Forms.TextBox txtScoreUser; 249 | private System.Windows.Forms.Label label1; 250 | private System.Windows.Forms.Button btnScore; 251 | private System.Windows.Forms.GroupBox groupRecommend; 252 | private System.Windows.Forms.TextBox txtRecommendNum; 253 | private System.Windows.Forms.Label label3; 254 | private System.Windows.Forms.TextBox txtRecommendUser; 255 | private System.Windows.Forms.Label label4; 256 | private System.Windows.Forms.Button btnRecommend; 257 | private System.Windows.Forms.RichTextBox rtbOutput; 258 | private System.ComponentModel.BackgroundWorker bgScore; 259 | private System.ComponentModel.BackgroundWorker bgRecommend; 260 | } 261 | } 262 | 263 | -------------------------------------------------------------------------------- /Example/MainForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Data; 5 | using System.Drawing; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Windows.Forms; 11 | using UserBehavior.Abstractions; 12 | using UserBehavior.Comparers; 13 | using UserBehavior.Objects; 14 | using UserBehavior.Parsers; 15 | using UserBehavior.Raters; 16 | using UserBehavior.Recommenders; 17 | 18 | namespace Example 19 | { 20 | public partial class MainForm : Form 21 | { 22 | IRecommender recommender; 23 | string savedModel = "recommender.dat"; 24 | 25 | public MainForm() 26 | { 27 | InitializeComponent(); 28 | 29 | IRater rate = new LinearRater(-4, 2, 3, 1); 30 | IComparer compare = new CorrelationUserComparer(); 31 | 32 | recommender = new UserCollaborativeFilterRecommender(compare, rate, 50); 33 | 34 | if (File.Exists(savedModel)) 35 | { 36 | try 37 | { 38 | recommender.Load(savedModel); 39 | rtbOutput.Text = "Loaded model from file"; 40 | EnableForm(true); 41 | } 42 | catch 43 | { 44 | rtbOutput.Text = "Saved model is corrupt"; 45 | } 46 | } 47 | 48 | //RecommenderTests.TestAllRecommenders(); 49 | //RecommenderTests.FindBestRaterWeights(); 50 | } 51 | 52 | private void btnLoadTrain_Click(object sender, EventArgs e) 53 | { 54 | if (!bgWorker.IsBusy && ofdGetUserBehaviors.ShowDialog() == DialogResult.OK) 55 | { 56 | rtbOutput.Text = "Training..."; 57 | bgWorker.RunWorkerAsync(ofdGetUserBehaviors.FileName); 58 | EnableForm(false); 59 | } 60 | } 61 | 62 | private void bgWorker_DoWork(object sender, DoWorkEventArgs e) 63 | { 64 | UserBehaviorDatabaseParser parser = new UserBehaviorDatabaseParser(); 65 | UserBehaviorDatabase db = parser.LoadUserBehaviorDatabase(e.Argument as string); 66 | 67 | recommender.Train(db); 68 | recommender.Save(savedModel); 69 | } 70 | 71 | private void bgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) 72 | { 73 | rtbOutput.Text = "Finished training\r\nModel saved to file"; 74 | EnableForm(true); 75 | } 76 | 77 | private void EnableForm(bool enabled) 78 | { 79 | groupRecommend.Enabled = enabled; 80 | groupScore.Enabled = enabled; 81 | groupTrain.Enabled = enabled; 82 | } 83 | 84 | private void btnScore_Click(object sender, EventArgs e) 85 | { 86 | if (!bgScore.IsBusy) 87 | { 88 | int userId; 89 | int articleId; 90 | int.TryParse(txtScoreUser.Text, out userId); 91 | int.TryParse(txtScoreArticle.Text, out articleId); 92 | 93 | if (userId >= 1 && userId <= 3000 && articleId >= 1 && articleId <= 3000) 94 | { 95 | bgScore.RunWorkerAsync(new GetRating { UserID = userId, ArticleID = articleId }); 96 | EnableForm(false); 97 | } 98 | else 99 | { 100 | MessageBox.Show("Invalid User ID or Article ID!"); 101 | } 102 | } 103 | } 104 | 105 | private void bgScore_DoWork(object sender, DoWorkEventArgs e) 106 | { 107 | GetRating args = e.Argument as GetRating; 108 | e.Result = recommender.GetRating(args.UserID, args.ArticleID); 109 | } 110 | 111 | private void bgScore_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) 112 | { 113 | rtbOutput.Text = "Predicted rating: " + ((double)e.Result).ToString("0.00"); 114 | EnableForm(true); 115 | } 116 | 117 | private void btnRecommend_Click(object sender, EventArgs e) 118 | { 119 | if (!bgRecommend.IsBusy) 120 | { 121 | int userId; 122 | int ratings; 123 | int.TryParse(txtRecommendUser.Text, out userId); 124 | int.TryParse(txtRecommendNum.Text, out ratings); 125 | 126 | if (userId >= 1 && userId <= 3000 && ratings >= 1 && ratings <= 100) 127 | { 128 | bgRecommend.RunWorkerAsync(new GetRecommendation { UserID = userId, Ratings = ratings }); 129 | EnableForm(false); 130 | } 131 | else 132 | { 133 | MessageBox.Show("Invalid User ID or Recommendation Count!"); 134 | } 135 | } 136 | } 137 | 138 | private void bgRecommend_DoWork(object sender, DoWorkEventArgs e) 139 | { 140 | GetRecommendation args = e.Argument as GetRecommendation; 141 | e.Result = recommender.GetSuggestions(args.UserID, args.Ratings); 142 | } 143 | 144 | private void bgRecommend_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) 145 | { 146 | List suggestions = e.Result as List; 147 | 148 | rtbOutput.Text = "Recommendations:"; 149 | foreach (Suggestion suggestion in suggestions) 150 | { 151 | rtbOutput.Text += "\r\n" + suggestion.ArticleID; 152 | } 153 | 154 | EnableForm(true); 155 | } 156 | } 157 | 158 | class GetRating 159 | { 160 | public int UserID { get; set; } 161 | public int ArticleID { get; set; } 162 | } 163 | 164 | class GetRecommendation 165 | { 166 | public int UserID { get; set; } 167 | public int Ratings { get; set; } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Example/MainForm.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 17, 17 122 | 123 | 124 | 183, 17 125 | 126 | 127 | 289, 17 128 | 129 | 130 | 385, 17 131 | 132 | 133 | 134 | 135 | AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAA 136 | AABgAAAAAQAgAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 137 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtxeCQXcXScD3FxRAdtbeQDb 138 | WpoA21qvANtaugDbWrsA21qxANtanAHbW30D3FxUBdxdKwbcXgsAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 139 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 140 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtxeBgXcXS4B21tzAdtatQbc 141 | XuAV3mf1KOF0/j3kgf9I5Yn/XOiW/17ol/9L5or/P+SD/yrhdf4V3mj2B9xf4wHbW7oB21t6BNxdNAbc 142 | XggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 143 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfcXggE3F1FAdtbpQjc 144 | X+go4XT+YOmY/5nxvf/I99v+4/vt/vL99//4/vr+/f/+//7//v/4/vv+8/33/+X77v7J99z+n/LB/2Tp 145 | m/8q4XX/Ct1h7AHbW60D3FxNB9xeCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 146 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI3GABBdxdNALb 147 | W6UM3WLxROWG/5rxvv/f++v//P/9//////////////////////////////////////////////////// 148 | //////////////z//f/j++3+pPLE/0rliv8Q3WX1AdtbrwXcXT0I3F8DAAAAAAAAAAAAAAAAAAAAAAAA 149 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfc 150 | Xw4D3FxzBtxe5Drjf/+n88b/8P31//////////////////////////////////////////////////// 151 | //////////////////////////////////////////////T9+P+w9Mz/QuSF/wfcX+oD3Fx/B9xeEgAA 152 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 153 | AAAAAAAABtxeHwLbXKYW3mj6e+yq/+b77/7///////////////////////////////////////////// 154 | ////////////////////////////////////////////////////////////////////////7Pzz/4bu 155 | sf8b32z8AttbsgbcXigAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 156 | AAAAAAAAAAAAAAAAAAAG3F4sA9xcwSrhdf+x9Mz//P/9//////////////////////////////////// 157 | //////////////////////////////////////////////////////////////////////////////// 158 | //////////////3//v+89tT+M+J7/wLbXM0F3F43AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 159 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbcXi0C21zKOeN//8z43v////////////////////////////// 160 | //////////////////////////////////////////////////////////////////////////////// 161 | ////////////////////////////////////////1fnk/kfliP8E3F3VBdxeOAAAAAAAAAAAAAAAAAAA 162 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtxeIQPcXMI643//0/ni/v////////////////// 163 | //////////////////////////////////////////////////////////////f++v/5/vv///////// 164 | /////////////////////////////////////////////////////////////+H77P9K5Yr/Attczwbc 165 | XiwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH3F4OAttbqCzhdv/O+N////////// 166 | ///////////////////6/vz+/f/9/////////////////////////////////////////////////936 167 | 6f6q88j+/P/9//////////////////////////////////////////////////////////////////// 168 | ///X+eX+N+N9/wLbW7gG3F4WAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvdYgED3Fx3Ft5o+7H0 169 | zf////////////////////////////n++/+99tT+9v75//////////////////////////////////// 170 | /////////////+z88v5J5Yr+svTN/v////////////////////////////////////////////////// 171 | ////////////////////////wfbX/h/gbv4C3FyJCNxfBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXc 172 | XTgH3F/mge2u//3//f///////////////////////////7321f527Kb+/P/9//////////////////// 173 | //////////////////////////////v+/f9V55H+KuF1/8/44P7///////////////////////////// 174 | /////////////////////////////////////////////5Dvt/8J3GDuBdxdSAAAAAAAAAAAAAAAAAAA 175 | AAAAAAAAB9xeCQLbW6o+5IL/6fzx////////////////////////////7f30/0fliP5+7az+//////// 176 | //////////////////////////////////////////////////997av+ANtZ/2vrn/77/vz///////// 177 | //////////////////////////////////////////////////////////////D99f9N5oz/Attbuwbc 178 | XhAAAAAAAAAAAAAAAAAAAAAABNxdSw3dYvOp88f/////////////////////////////////lPC6/gbc 179 | Xv+f8cH+//////////////////////////////////////////////////////////+t9Mr+X+iX/sPt 180 | 1f79/v3///////////////////////////////////////////////////////////////////////// 181 | //+89dP/Ft5o+QPcXF0AAAAAAAAAAAAAAAAG3F4HAdtbq0blh//x/fb///////////////////////// 182 | ///7/fz/rOzH/XTqpf7P+eD+//////////////////////////////////////////////////////// 183 | ///j9Or+h72h/rfUxf7i/O3+5PHq/eny7f7///////////////////////////////////////////// 184 | ///////////////////5/vv/WuiU/wHbW70G3F4OAAAAAAAAAAAF3F01Cdxg66Dywv////////////// 185 | ///////////////////2+ff/aaaH/mGigf6iybX+2enh/vf6+f////////////////////////////// 186 | /////////v///8vg1f5LlW/+RpFr/s3v3P6A7639ttnG/U+Xcv7H3tL+/v/+//////////////////// 187 | ////////////////////////////////////////s/TO/w/dZPME3F1FAAAAAAAAAAAB21t8K+F2/+L7 188 | 7P7///////////////////////////3//v/w/fX+b6uM/gBnMP8FbDj/HntM/0yWcP6Lu6L+xt7S/u/2 189 | 8v/+//7////////////z+PX/lsGr/iB8Tf8Kbjv/rs++/oXvsP495YL9ttrH/g1wPf8ee0v/mcOu/vX5 190 | 9///////////////////////////////////////////////////////7fzz/znjf/8B21uQAAAAAAbc 191 | Xg0B21u8Zemb//3//v///////////////////////////+j88P+O8bf+ocy2/gdtOf8Cazb/AWo1/wBp 192 | M/8CajX/EnRC/zmLYf5zrI/+1+jf/vT49v5io4L+B205/wBoMv9UmXb+yPLa/iHgb/845H//uN3J/RFz 193 | Qf4AaTT/Cm87/2Skg/7g7eb//////////////////////////////////////////////////////3rs 194 | qf8D3FzMBtxeFwXcXS8I3F/lofLC/////////////////////////////////6vzyP4x43r+u+XN/iB7 195 | Tf8BajX/A2s2/wNrNv8Dazb/Amo1/wBnMf82iF3+0+rd/t3t5fwfe0z9AGkz/xN0Q/+62Mj+a+yg/gHb 196 | W/8v43n+v+TQ/Rx5Sv8CajX/Ams1/wFqNf82iV/+uNbH/vz+/f////////////////////////////// 197 | /////////////7f10P8O3WPuBNxdQQLcXFsY3mn4zvjf/v//////////////////////////+P76/1Xn 198 | kf4J3WD/qu7G/kmSbf4AaTP/A2s2/wNrNv8Dazb/AWo0/yN9T/+328f+ifGz/Mbp1foifU/9AGgy/2ek 199 | hf628s/+Ft5o/wPcXP8j4XH+vObO/iJ8Tv8BajX/A2s2/wNrNv8AaTP/FnZF/6vOvP7///////////// 200 | /////////////////////////////9z66f4i4HD8AdtbcgHbW4Yu4nj/6Pzw/v////////////////// 201 | ////////0vni/hrfa/8B21v/gO6t/nuxlv4AaTP/A2s2/wNrNv8CajX/EnNC/6bMuP6E77D+KuJ2/sPp 202 | 1PshfE79GnhI/8Dfzv5R6I/+A9xc/wTcXf8h4XD/vOjP/SR9UP4BajT/A2s2/wJrNv8CajX/UZh0/t/s 203 | 5f////////////////////////////////////////////L99v495IH/ANtanQDbWqdE5Yb/9v75/v// 204 | ////////////////////////7fzz/13olv8C21v/SueL/qTOuP4Jbjr/A2s2/wJrNf8Ibjr/ibmh/qDx 205 | wf4Q3mX/I+Fx/8nu2f4tglf+eK6T/qDxwv4M3WL/Bdxe/wXcXf8W32n+uOzO/DOGXP4AaTT/Amo1/wVs 206 | N/9ko4P+4PDo/uT87v7+//////////////////////////////////////////z//f9X55L/ANtavgDb 207 | Wrxb6JX//f/+/////////////////////////////////+b87/9L5or+HuBu/7LkyP4hfE7/AWo0/wJr 208 | Nf9sp4n+t/HP/iHgb/8C21v/JeFy/8vv2/9anHv/vOPO/jnkf/8D3Fz/Btxe/wXcXv8R3mX/tO7M/juK 209 | Yv8AaDL/CW47/3Osj/7l9ez+g+6v/rH0zP7///////////////////////////////////////////// 210 | //9k6Zv/ANta0gDbWsdg6Zj//v/+///////////////////////////////////////d+un+SuaK/pzt 211 | vv5Kk27+AGcx/06Wcv7D7dX+OOR+/gPcXP8F3F3/HeBt/sbv2P2918r+hO2v/gbcXv8G3F7/Btxe/wXc 212 | Xv8P3mT/sO7K/TyLY/0Jbjr/iLig/uH36v5j6pv+KuF1/t/66v7///////////////////////////// 213 | //////////////////947Kj/Adtb3ADbWshg6Zj//v/+//////////////////////////////////// 214 | ////////1/nl/6r0yP52r5L+L4RY/sPk0v5U6JH+Attc/wTcXP8B21v/Ed5l/sz33fzU9+L+JeFy/gPc 215 | XP8F3F3/Bdxd/wPcXP8E3F3/oO7A/GOigv6XwKv+1/fk/k7njf4B21v/YemZ/vz//f////////////// 216 | //////////////////////////////////987Kr/Attb3QDbWr9d6Jb//f/+//////////////////// 217 | //////////////////////////////T/+P7C3M/+t9nH/m7sof4F3F3/Ed1l/yvhdv5V55H+iu6z/uv8 218 | 8v7j++3/pvLG/ZnxvfyV8Lv/lPC6/4Pur/x37Kf8yPbb/t7q5P7F9tn+OuSA/gPcXP8J3GD/qPPG/v// 219 | //////////////////////////////////////////////////5m6Zz+ANta1ADbWqtH5Yj/9/76/v// 220 | ///////////////////////////////////////////////////+/v7/v/bV/mjqnf6T8Ln+wvbY/uX7 221 | 7v76/vz///////////////////////////////////////////////////////r+/P9a6JT+Adtb/wTc 222 | Xf8o4XT+4Pvr/v////////////////////////////////////////////////3//v9b6JX/ANtawgDb 223 | Wowx4nr/6/zy/v/////////////////////////////////////////////////////+////9f75/vz/ 224 | /f///////////////////////////////////////////////////////////////////////v7+//b4 225 | 9/2f8cH+Ct1g/wLbW/9i6Zn+/P/9//////////////////////////////////////////////////T9 226 | +P5B5IT/ANtapALbXGMb32v60/nj/v////////////////////////////////////////////////// 227 | //////////////////////////////////////////////////////////////////////////////// 228 | ////////+fz6/6fLuP3a9OX+NuN9/gncYP+t88r+//////////////////////////////////////// 229 | /////////////+H76/4m4HP9AdtbegXcXTYK3WDpqvPI//////////////////////////////////// 230 | //////////////////////////////////////////////////////////////////////////////// 231 | ////////////////////////+fv6/12gfv6+2Mr+jfC1/i3hd//k++7///////////////////////// 232 | /////////////////////////////7/21f8R3WXyA9xcSQbcXhEB21vFcOui//////////////////// 233 | //////////////////////////////////////////////////////////////////////////////// 234 | ////////////////////////////////////////8vf1/jeKYP5ko4P+1fjj/o3vtf78//3///////// 235 | /////////////////////////////////////////////4XusP8E3F3UBtxeHQAAAAAB21uINeN8/+r8 236 | 8f////////////////////////////////////////////////////////////////////////////// 237 | ////////////////////////////////////////////////////////7vXy/zOHXP8aeEj/zuTZ/vT/ 238 | +f7/////////////////////////////////////////////////////8v33/0Plhf8A21qcBtxeAgAA 239 | AAAE3F1ADt1j8bD0zP////////////////////////////////////////////////////////////// 240 | ////////////////////////////////////////////////////////////////////////4+7o/iR+ 241 | UP4AaTP/fbKX/v//////////////////////////////////////////////////////////wfbX/xXe 242 | aPgD3FxRAAAAAAAAAAAG3F4MAdtbuVXnkf/3/vn///////////////////////////////////////// 243 | //////////////////////////////////////////////////////////////////////////////// 244 | ////////3Orj/x57S/8AaDL/Z6aG/u/89P/3/vr//v////////////////////////////////////// 245 | ///9//3/aOqd/wLbW8kG3F4UAAAAAAAAAAAAAAAABNxcWxLeZvm49dH///////////////////////// 246 | //////////////////////////////////////////////////////////////////////////////// 247 | ////////////////////////zOHW/RBzQf4he03/st7G/lDnjv5I5Yj+b+ui/prxvv7C9tf+9P34/v// 248 | ///////////////////L+N3/Ht9t/QLbXG4AAAAAAAAAAAAAAAAAAAAABtxeEALbW7tO5o3/8f32//// 249 | //////////////////////////////////////////////////////////////////////////////// 250 | ////////////////////////////////////////w9zP/xByQP+JuqH+gO6t/gPcXP8C21v/Attb/wTc 251 | XP9e6Jf+8P31/v////////////////f++v9f6Jf/AttbywbcXhkAAAAAAAAAAAAAAAAAAAAAAAAAAATc 252 | XUoL3WHwlvC7//////////////////////////////////////////////////////////////////// 253 | ////////////////////////////////////////////////////////r9C//UqTbv7B7dT+K+J2/gjc 254 | X/8F3F3/Attb/0Dkg/7e+un+/////////////////////6Xyxf8P3WT2BNxdWwAAAAAAAAAAAAAAAAAA 255 | AAAAAAAAAAAAAAfcXwQC3FyOIeBv/sX32v7///////////////////////////////////////////// 256 | ////////////////////////////////////////////////////////////////////////tdPE/rrW 257 | yP7w/vX+wvbY/q70yv6X8Lz+g+2v/tT54///////////////////////1Pnj/i3hd/8C21ugB9xfCQAA 258 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG3F4ZAttbvj3kgf/f+ur///////////////////////// 259 | //////////////////////////////////////////////////////////////////////////////// 260 | ////////7fTx/vn7+v/////////////////////////////////////////////////n/O/+TeaM/wPc 261 | XMwG3F4jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABdxeMgTcXdZP5o3/5Pvt/v// 262 | //////////////////////////////////////////////////////////////////////////////// 263 | /////////////////////////////////////////////////////////////////////////////+79 264 | 9P9j6Zr/Bdxd4QXcXUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXc 265 | XUME3F3eUOaO/+H76/////////////////////////////////////////////////////////////// 266 | //////////////////////////////////////////////////////////////////////////////// 267 | ////////5/zv/mPpmv8H3F/mBNxdUQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 268 | AAAAAAAAAAAAAAAAAAAF3F1DBNxd2EHkhP/K+N3+//////////////////////////////////////// 269 | //////////////////////////////////////////////////////////////////////////////// 270 | ///////////////////V+eT+TeaM/wXcXeEE3F1RDd1jAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 271 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABdxeNALbXMIk4HL+m/G+//T9+P////////////// 272 | //////////////////////////////////////////////////////////////////////////////// 273 | ////////////////////////9/76/6vzyP8u4nj/A9xczQXcXUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 274 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbcXhsC21yUDN1i81Xn 275 | kf/E99n++v78//////////////////////////////////////////////////////////////////// 276 | //////////////////////////////z//f/O+N//YOmY/xDdZfYC21yhBtxeIwAAAAAAAAAAAAAAAAAA 277 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 278 | AAAH3F8HBNxdUALbW8QY3mr7Yuma/7721f/y/ff///////////////////////////////////////// 279 | ///////////////////////////////////0/fj/w/bY/2zqoP8f3278A9xczATcXFsH3F8KAAAAAAAA 280 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 281 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbcXhQD3FxmAttbxRPeZvdC5IX/gu2u/7321P/h++z+9f74//3/ 282 | /v///////////////////////f/+//b++f/k++7+wPbW/ojusv9G5Yf/Fd5o+QLbW8sC21xvBtxeGQAA 283 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 284 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtxeEQPcXEwA21qYA9xc0hDd 285 | ZPIm4HP9QuSF/13olv9r6p/+gO2t/4Htrv9u66H+XeiW/0Xlhv8q4XX+Ed5l8wTcXdYB21qeA9xcUwbc 286 | XhUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 287 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 288 | AAAG3F4DBtxeGgPcXEcB21t5ANtapADbWsMB21rXAttb4QLbW+EB21vYANtaxgDbWqgB21t9A9xcSwbc 289 | Xh4G3F4EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 290 | AAAAAAAAAAAAAP//+B///wAA//+AAf//AAD//AAAP/8AAP/wAAAP/wAA/+AAAAf/AAD/gAAAAf8AAP8A 291 | AAAA/wAA/gAAAAB/AAD8AAAAAD8AAPgAAAAAHwAA+AAAAAAPAADwAAAAAA8AAOAAAAAABwAA4AAAAAAH 292 | AADAAAAAAAMAAMAAAAAAAwAAwAAAAAABAACAAAAAAAEAAIAAAAAAAQAAgAAAAAABAAAAAAAAAAAAAAAA 293 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAB 294 | AACAAAAAAAEAAIAAAAAAAQAAgAAAAAABAADAAAAAAAMAAMAAAAAAAwAA4AAAAAAHAADgAAAAAAcAAPAA 295 | AAAADwAA8AAAAAAPAAD4AAAAAB8AAPwAAAAAPwAA/gAAAAB/AAD/AAAAAP8AAP+AAAAB/wAA/8AAAAP/ 296 | AAD/8AAAD/8AAP/8AAA//wAA//8AAP//AAD///AP//8AACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAA 297 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANlRDADa 298 | UzoB21hzCNxfpBLeZsQb32zTHN9s0xPeZsUJ3GCnAdtZdwDaVD0A2VEOAAAAAAAAAAAAAAAAAAAAAAAA 299 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANpUFgHb 300 | WWQY3mm5ReWH63fsp/yh8sL/u/XT/8r33f/L+N3/vPbU/6Pyw/967Kn9R+WI7Rrfa70C21lpANpUGQAA 301 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANtXBwDb 302 | WFUd323Iaeqe+7721f/x/fb////////////////////////////////////////////y/ff/w/fY/27r 303 | ofwh4G/OANtZXQDaVwkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADa 304 | VxUI3F+RTOaL9MD21v/7/vz///////////////////////////////////////////////////////// 305 | /////////P/9/8b32v9S54/2CtxgmgDaVxoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 306 | AAAA2lcdEN1kr3Xspv7r/PL///////////////////////////////////////////////////////// 307 | /////////////////////////////+/99f9+7av/E95muADbVyMAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 308 | AAAAAAAAANtXFhDdZLCF7rD/9/76//////////////////////////////////////////////////j+ 309 | +v/9//7///////////////////////////////////////r++/+S8Lj/FN5nuQDaVxwAAAAAAAAAAAAA 310 | AAAAAAAAAAAAAADbVwcH3F+Td+yn/vj++v////////////j++v/0/fj+//////////////////////// 311 | ////////xPfZ/sz43v7///////////////////////////////////////////r+/P+B7a3/Ct1hngDa 312 | VwoAAAAAAAAAAAAAAAAAAAAAANtZWU/mjfXs/PL/////////////////s/TO/tb55P7///////////// 313 | ///////////////////E99n+TOaM/t/76v////////////////////////////////////////////H9 314 | 9v9Y55P4ANtZZQAAAAAAAAAAAAAAAADaVRge327KwvbY/////////////////9n65v9O5o3+4Pvr//// 315 | /////////////////////////////+L77P845H/+qfLH/v////////////////////////////////// 316 | /////////////8v43f8l4HLUANpVHwAAAAAAAAAAANtZaWvqn/z8//3/////////////////tO7M/nHo 317 | ov70/vj/////////////////////////////////9Pz4/5XVsf7I5tb+6vfw/vH39P////////////// 318 | /////////////////////////v///3nsqP8B21t1AAAAAADZUg8Z32q9wfbX//////////////////// 319 | //+Zw63+Tplz/pTAqv7N4tf/8/j1/////////////////+z08P99spf+Xp9+/pjqu/6X27T9YaGB/tDj 320 | 2v//////////////////////////////////////y/je/yDgb8gA2VIWANpUP0fliO3y/fb///////// 321 | ////////1/vm/pLOrf4JbTr/Amo1/xV2RP8/jmb/e7GV/tLk2/7a6eH+RpJr/xFyQP+Vza7+PueD/4bZ 322 | qv4WdET+JX9R/6XKt/75+/r////////////////////////////3/vr/U+eQ8gDaVUwA21l7fOyq/f// 323 | //////////////3//v9w7KP+fd+m/il/U/8BajT/Amo1/wBoMf8deUn/q9nA/Z3KsvsAaDP/WJ16/njp 324 | p/4K3mH/h96s/SF7Tv8AaTP/DXE+/2+qjP7p8u3///////////////////////////+K7rP/Attbigrd 325 | YKym8sb/////////////////4fvr/yPgcf9d5pX+Tphy/gBpM/8CajX/EHJA/4XEov5p7Z/+hcaj+xt3 326 | SP6T1bD+I+Fx/wbdXv+C4Kn+Jn1R/gFqNP8AaTP/M4Zb/9fn3/7//////////////////////////7P0 327 | zv8P3WS7Ft5ozML22P/////////////////y/ff/ZOmb/zXkff5wtJD+AWk0/wdtOf90s5L+XOeV/ibj 328 | dP+Py6r+bKqK/mDnmP4D3Fz/Bd1e/3vjpv0vg1n/AGgy/0COZv+14Mj+4vzs/v////////////////// 329 | ////////zPje/xvfbNke323bz/jg///////////////////////q/PH/a+yf/n/Oof4Nbz7/XqJ//nbn 330 | pf4H3V//IOFv/7Dfxf6P3bD+Et9m/wPcXP8C3Fz/dOOi/jSGXP5NlXD/ruXF/mDqmP7X+eX+//////// 331 | ///////////////////Y+ub/JeBy5x7fbtzP+OD////////////////////////////n/PD/tufL/mim 332 | hv6C4ar+EN5l/wrcYP8x4nr+0vfh/Xfsp/4q4XX+LOF2/yDgb/5556f9lsCr/qPmwP4v43n/UueP/vn+ 333 | +////////////////////////////9n65/8m4HPnGN5pzsT32f////////////////////////////// 334 | ///7/fz/zfHc/m3rof6B7a3+tPTO/t366f/6/vz/8P32/un88f7n/PD/4Pvr/uz88//Y+eX+KuJ2/wPc 335 | XP+b8b/+////////////////////////////////zfjf/xzfbNsL3WGwqvPI//////////////////// 336 | ///////////////////4/vr/+P77/////////////////////////////////////////////Pz8/9rw 337 | 5P4/5IP/Ht9t/9n65v7///////////////////////////////+29dD/Ed1lwADbWoGB7a3+//////// 338 | //////////////////////////////////////////////////////////////////////////////// 339 | ///x9/T/lL+p/o3qtP5d6Jf/+v78/////////////////////////////////4/vtv8D3FyRANpURk7m 340 | jPD1/vn///////////////////////////////////////////////////////////////////////// 341 | /////////////+z08P9DkGn+qNe9/sv53v7////////////////////////////////5/vv/WeiU9ADa 342 | VVMA2VITH99uxcr33P////////////////////////////////////////////////////////////// 343 | ////////////////////////5e/q/iN+T/5opof+/f7+/////////////////////////////////9P5 344 | 4/8l4HLPANlSGgAAAAAB21tzdeym/v7//v////////////////////////////////////////////// 345 | ///////////////////////////////////b6uL+GXhI/1qhfP7A9db+0/ni/uz88//6/vz///////// 346 | ////////g+6v/wPcXIAAAAAAAAAAAADaVR8k4HLUzPje//////////////////////////////////// 347 | /////////////////////////////////////////////83i1/4zhlz+f9el/hffaf8W3mj/QuSF/tH5 348 | 4f7//////////9X55P8t4XfcANpVJwAAAAAAAAAAAAAAAADbWmZb6JX58v33//////////////////// 349 | ////////////////////////////////////////////////////////xNzQ/pHAqP6C7q7+MuJ7/ybg 350 | c/+K77P++v78///////2/vn/Zemb/AHbWnMAAAAAAAAAAAAAAAAAAAAAANtXCwvdYaOG7rH//P/9//// 351 | ///////////////////////////////////////////////////////////////////p8u3+7vXy//j+ 352 | +//u/fT/5Pvu//j++////////f/+/5Lwuf8P3WSuANpXEAAAAAAAAAAAAAAAAAAAAAAAAAAAANpXHxbe 353 | aMCW8Lv//P/9//////////////////////////////////////////////////////////////////// 354 | //////////////////////////////3//v+k8sT/G99syADaVyYAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 355 | AAAAAAAAANpXKRfeacGJ7rP/8/34//////////////////////////////////////////////////// 356 | ///////////////////////////////////2/vn/kvC5/xzfbMkA21cwAAAAAAAAAAAAAAAAAAAAAAAA 357 | AAAAAAAAAAAAAAAAAAAAAAAAANpXIA3dYqZd6Jb60fjh//7//v////////////////////////////// 358 | ////////////////////////////////////////1/nl/2fqnfsQ3WSvANpXJgAAAAAAAAAAAAAAAAAA 359 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANpXDQHbWmso4XTZfu2r/tL54v/5/vv///////// 360 | ///////////////////////////////////6/vz/1fnk/4XusP8u4njeA9tbcwDaVxAAAAAAAAAAAAAA 361 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADaVSME3Fx7JOByzVjn 362 | k/SO77b/t/XR/8/44P/b+uj/3Prp/9D44P+59dL/kfC4/1volfYm4HPQBdxdgQDaVScA21kBAAAAAAAA 363 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADZ 364 | TgEA2VIYANpVUQTcXJAR3mXAHt9t3SjhdOop4XXqHt9u3hLeZsIE3FyTANpVVQDZUhsA2VEBAAAAAAAA 365 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gf///AA///AAD//AAAP/gAAB/wAAAP4AAAB+AA 366 | AAfAAAADwAAAA4AAAAGAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAABgAAAAcAA 367 | AAHAAAAD4AAAB+AAAAfwAAAP+AAAH/wAAD//AAD//8AB///wD/8oAAAAEAAAACAAAAABACAAAAAAAAAE 368 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMQEAhLcYDJI5YmKc+uky4rus+eK77TndOulzUnl 369 | iY0T3WE0AMcMAwAAAAAAAAAAAAAAAAAAAAAAAAAAANZDDUTlhXec8b/h3Pro/vf++v/+//7//v/+//f+ 370 | +v/d+un+n/HB40blh3wA10YPAAAAAAAAAAAAAAAAANZADlfnkpfJ99z7/v/+/////////////v////v+ 371 | /P////////////7//v/M+N78XOiVnADWRBAAAAAAAIQAAUXlhnnK99z7+/78/+D76//+//7///////3/ 372 | /v+49dH+6/zy/////////////////8343/1I5Yh/ALAAAgncYDSd8b/h/////9j25P+v78r+//////// 373 | ////////qenE/snw2v75+/r/////////////////ovLD5Q/dZDpI5YmN3Pro//////+238j+S5hw/4O2 374 | nP/P4tj/utbH/2yyjf5616L+cKyN/tjo4P///////////+D76/9O5oyVdOulzvj++v/r/fL/ad+a/iB/ 375 | Tf8KbTr/bb+S/lymgP1JxH7/Q9iB/ht5SP9FkWr/6vPu///////5/vv/euyp1Yzvter/////9f75/4zu 376 | tP5AnGv+QaZv/z7ggf55z5/+Idxu/zPXd/5Bkmj+c8aY/uf77////////////5HvuPCN77Xr/v/+//// 377 | ///1/vj/tt/J/m/jn/597av+xPbY/onvs/+c7b7+g96q/l7pl//0/vj///////////+R8Ljwduyn0fj+ 378 | +v////////////v//f/1/vn//v//////////////9/n4/4vhr/6X8bz////////////5/vv/fOyq10zm 379 | i5Lf+ur//////////////////////////////////////+Lu6P90sZH+6Pnv////////////4vvs/1Hm 380 | jpoO3WM5ofLC5f/////////////////////////////////////a6OH/WKt+/nTopP/C99f//////6by 381 | xegT3mY+ALEAAknliYDO+OD9////////////////////////////////6vLu/7rnzf6N8Lb/1vnk/9T5 382 | 4/5N5ouHAL8AAwAAAAAA10YRXeiWodD44P3+/////////////////////////////////////////9X5 383 | 5P1i6ZmmANhKEwAAAAAAAAAAAAAAAADXSRFK5YqCpPLE6OL77P/6/vz////////////6/vz/4/vt/6jz 384 | x+lO5oyHANhMEwAAAAAAAAAAAAAAAAAAAAAAAAAAAMsaBBjeZTxR5o6YfO2q2JPwufKT8Lnyfe2r2VLn 385 | j5sa3mc+AMwfBQAAAAAAAAAAAAAAAPgfAADwDwAAwAMAAMADAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAA 386 | AAAAAAAAgAEAAIABAADAAwAA4AcAAPgfAAA= 387 | 388 | 389 | -------------------------------------------------------------------------------- /Example/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Windows.Forms; 6 | 7 | namespace Example 8 | { 9 | static class Program 10 | { 11 | /// 12 | /// The main entry point for the application. 13 | /// 14 | [STAThread] 15 | static void Main() 16 | { 17 | Application.EnableVisualStyles(); 18 | Application.SetCompatibleTextRenderingDefault(false); 19 | Application.Run(new MainForm()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Example")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Scott Clayton")] 12 | [assembly: AssemblyProduct("Recommender Example")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("a27ff3ec-ee8e-4ee8-b6f2-60572619cec7")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Example/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Example.Properties 12 | { 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources 26 | { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal Resources() 34 | { 35 | } 36 | 37 | /// 38 | /// Returns the cached ResourceManager instance used by this class. 39 | /// 40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 41 | internal static global::System.Resources.ResourceManager ResourceManager 42 | { 43 | get 44 | { 45 | if ((resourceMan == null)) 46 | { 47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Example.Properties.Resources", typeof(Resources).Assembly); 48 | resourceMan = temp; 49 | } 50 | return resourceMan; 51 | } 52 | } 53 | 54 | /// 55 | /// Overrides the current thread's CurrentUICulture property for all 56 | /// resource lookups using this strongly typed resource class. 57 | /// 58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 59 | internal static global::System.Globalization.CultureInfo Culture 60 | { 61 | get 62 | { 63 | return resourceCulture; 64 | } 65 | set 66 | { 67 | resourceCulture = value; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Example/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /Example/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Example.Properties 12 | { 13 | 14 | 15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] 17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase 18 | { 19 | 20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 21 | 22 | public static Settings Default 23 | { 24 | get 25 | { 26 | return defaultInstance; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Example/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/RecommenderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using UserBehavior.Comparers; 9 | using UserBehavior.Objects; 10 | using UserBehavior.Parsers; 11 | using UserBehavior.Raters; 12 | using UserBehavior.Recommenders; 13 | 14 | namespace Example 15 | { 16 | class RecommenderTests 17 | { 18 | public static void FindBestRaterWeights() 19 | { 20 | using (StreamWriter w = new StreamWriter("rater-weights.csv", true)) 21 | { 22 | w.WriteLine("down,up,view,dl,rmse,solved,total,precision,rank"); 23 | } 24 | 25 | var once = new User(0, ""); 26 | var options = new List(); 27 | 28 | for (double up = 0.0; up < 5.0; up += 1) 29 | { 30 | for (double down = -5.0; down <= 0.0; down += 1) 31 | { 32 | for (double dl = 0.0; dl < 5.0; dl += 1) 33 | { 34 | for (double view = 0.0; view < 5.0; view += 1) 35 | { 36 | options.Add(new { up, down, dl, view }); 37 | } 38 | } 39 | } 40 | } 41 | 42 | var dbp = new UserBehaviorDatabaseParser(); 43 | var db = dbp.LoadUserBehaviorDatabase("UserBehaviour.txt"); 44 | var sp = new DaySplitter(db, 3); 45 | var cp = new CorrelationUserComparer(); 46 | 47 | Parallel.ForEach(options, set => 48 | { 49 | try 50 | { 51 | var rate = new LinearRater(set.down, set.up, set.view, set.dl); 52 | var mfr = new MatrixFactorizationRecommender(20, rate); 53 | //var mfr = new UserCollaborativeFilterRecommender(cp, rate, set.features); 54 | 55 | mfr.Train(sp.TrainingDB); 56 | 57 | var score = mfr.Score(sp.TestingDB, rate); 58 | var results = mfr.Test(sp.TestingDB, 100); 59 | 60 | lock (once) 61 | { 62 | using (StreamWriter w = new StreamWriter("rater-weights.csv", true)) 63 | { 64 | w.WriteLine(set.down + "," + set.up + "," + set.view + "," + set.dl + "," + score.RootMeanSquareDifference + "," + results.UsersSolved + "," + results.TotalUsers + "," + results.AveragePrecision + "," + results.AverageRecall); 65 | } 66 | } 67 | } 68 | catch (Exception ex) 69 | { 70 | File.WriteAllText("errors.txt", ex.ToString()); 71 | } 72 | }); 73 | } 74 | 75 | public static void TestAllRecommenders() 76 | { 77 | UserBehaviorDatabaseParser dbp = new UserBehaviorDatabaseParser(); 78 | UserBehaviorDatabase db = dbp.LoadUserBehaviorDatabase("UserBehaviour.txt"); 79 | 80 | //var ubt = new UserBehaviorTransformer(db); 81 | //var uart = ubt.GetUserArticleRatingsTable(); 82 | //uart.SaveSparcityVisual("sparcity.bmp"); 83 | //uart.SaveUserRatingDistribution("distrib.csv"); 84 | //uart.SaveArticleRatingDistribution("distriba.csv"); 85 | 86 | var rate = new LinearRater(); 87 | var sp = new DaySplitter(db, 5); 88 | var uc = new CorrelationUserComparer(); 89 | 90 | //var rr = new RandomRecommender(); 91 | //rr.Train(sp.TrainingDB); 92 | //ScoreResults scores5 = rr.Score(sp.TestingDB, rate); 93 | //TestResults results5 = rr.Test(sp.TestingDB, 30); 94 | 95 | var ubc = new UserCollaborativeFilterRecommender(uc, rate, 30); 96 | var mfr = new MatrixFactorizationRecommender(30, rate); 97 | var icf = new ItemCollaborativeFilterRecommender(uc, rate, 30); 98 | var hbr = new HybridRecommender(ubc, mfr, icf); 99 | 100 | hbr.Train(sp.TrainingDB); 101 | ScoreResults scores1 = hbr.Score(sp.TestingDB, rate); 102 | TestResults results1 = hbr.Test(sp.TestingDB, 30); 103 | 104 | ubc = new UserCollaborativeFilterRecommender(uc, rate, 30); 105 | mfr = new MatrixFactorizationRecommender(30, rate); 106 | icf = new ItemCollaborativeFilterRecommender(uc, rate, 30); 107 | 108 | ubc.Train(sp.TrainingDB); 109 | ScoreResults scores2 = ubc.Score(sp.TestingDB, rate); 110 | TestResults results2 = ubc.Test(sp.TestingDB, 30); 111 | 112 | mfr.Train(sp.TrainingDB); 113 | ScoreResults scores3 = mfr.Score(sp.TestingDB, rate); 114 | TestResults results3 = mfr.Test(sp.TestingDB, 30); 115 | 116 | icf.Train(sp.TrainingDB); 117 | ScoreResults scores4 = icf.Score(sp.TestingDB, rate); 118 | TestResults results4 = icf.Test(sp.TestingDB, 30); 119 | 120 | using (StreamWriter w = new StreamWriter("results.csv")) 121 | { 122 | w.WriteLine("model,rmse,users,user-solved,precision,recall"); 123 | w.WriteLine("UCF," + scores2.RootMeanSquareDifference + "," + results2.TotalUsers + "," + results2.UsersSolved + "," + results2.AveragePrecision + "," + results2.AverageRecall); 124 | w.WriteLine("SVD," + scores3.RootMeanSquareDifference + "," + results3.TotalUsers + "," + results3.UsersSolved + "," + results3.AveragePrecision + "," + results3.AverageRecall); 125 | w.WriteLine("ICF," + scores4.RootMeanSquareDifference + "," + results4.TotalUsers + "," + results4.UsersSolved + "," + results4.AveragePrecision + "," + results4.AverageRecall); 126 | w.WriteLine("HR," + scores1.RootMeanSquareDifference + "," + results1.TotalUsers + "," + results1.UsersSolved + "," + results1.AveragePrecision + "," + results1.AverageRecall); 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Example/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skotz/cp-user-behavior/882eac66800af6c589fb20f094bb9d233bfbd8a2/Example/icon.ico -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Scott Clayton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C# Recommendation Engine 2 | 3 | This project and [corresponding article](https://www.codeproject.com/Articles/1232150/Building-an-Article-Recommendation-Engine) won first place in Code Project's 2018 Machine Learning and Artificial Intelligence Competition. 4 | It demonstrates user-based and item-based collaborative filtering with matrix factorization using 5 | the user behavior data provided by Code Project for the challenge. 6 | 7 | ### Usage 8 | 9 | ```C# 10 | IRater rate = new LinearRater(-4, 2, 3, 1); 11 | IComparer compare = new CorrelationUserComparer(); 12 | IRecommender recommender = new UserCollaborativeFilterRecommender(compare, rate, 50); 13 | 14 | UserBehaviorDatabaseParser parser = new UserBehaviorDatabaseParser(); 15 | UserBehaviorDatabase db = parser.LoadUserBehaviorDatabase("UserBehavior.txt"); 16 | ISplitter split = new DaySplitter(db, 5); 17 | 18 | recommender.Train(split.TrainingDB); 19 | 20 | ScoreResults scores = recommender.Score(split.TestingDB, rate); 21 | TestResults results = recommender.Test(split.TestingDB, 30); 22 | 23 | List suggestions = recommender.GetSuggestions(someUserId, numberOfRecommendations); 24 | ``` 25 | -------------------------------------------------------------------------------- /User Behavior.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Recommender", "User Behavior\Recommender.csproj", "{DB5A3FEF-D18F-4901-9B4F-959E403BD21B}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Example\Example.csproj", "{A27FF3EC-EE8E-4EE8-B6F2-60572619CEC7}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {DB5A3FEF-D18F-4901-9B4F-959E403BD21B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {DB5A3FEF-D18F-4901-9B4F-959E403BD21B}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {DB5A3FEF-D18F-4901-9B4F-959E403BD21B}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {DB5A3FEF-D18F-4901-9B4F-959E403BD21B}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {A27FF3EC-EE8E-4EE8-B6F2-60572619CEC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {A27FF3EC-EE8E-4EE8-B6F2-60572619CEC7}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {A27FF3EC-EE8E-4EE8-B6F2-60572619CEC7}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {A27FF3EC-EE8E-4EE8-B6F2-60572619CEC7}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /User Behavior/Abstractions/IClassifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using UserBehavior.Objects; 7 | using UserBehavior.Parsers; 8 | 9 | namespace UserBehavior.Recommenders 10 | { 11 | public interface IRecommender 12 | { 13 | void Train(UserBehaviorDatabase db); 14 | 15 | List GetSuggestions(int userId, int numSuggestions); 16 | 17 | double GetRating(int userId, int articleId); 18 | 19 | void Save(string file); 20 | 21 | void Load(string file); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /User Behavior/Abstractions/IComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Comparers 8 | { 9 | public interface IComparer 10 | { 11 | double CompareVectors(double[] userFeaturesOne, double[] userFeaturesTwo); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /User Behavior/Abstractions/IRater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using UserBehavior.Objects; 7 | 8 | namespace UserBehavior.Abstractions 9 | { 10 | public interface IRater 11 | { 12 | double GetRating(List actions); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /User Behavior/Abstractions/ISplitter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Parsers 8 | { 9 | public interface ISplitter 10 | { 11 | UserBehaviorDatabase TrainingDB { get; } 12 | 13 | UserBehaviorDatabase TestingDB { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /User Behavior/Comparers/CoRatedCosineUserComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Comparers 8 | { 9 | public class CoRatedCosineUserComparer : IComparer 10 | { 11 | public double CompareVectors(double[] userFeaturesOne, double[] userFeaturesTwo) 12 | { 13 | double sumProduct = 0.0; 14 | double sumOneSquared = 0.0; 15 | double sumTwoSquared = 0.0; 16 | 17 | for (int i = 0; i < userFeaturesOne.Length; i++) 18 | { 19 | // Only compare articles rated by both users 20 | if (userFeaturesOne[i] != 0 && userFeaturesTwo[i] != 0) 21 | { 22 | sumProduct += userFeaturesOne[i] * userFeaturesTwo[i]; 23 | sumOneSquared += Math.Pow(userFeaturesOne[i], 2); 24 | sumTwoSquared += Math.Pow(userFeaturesTwo[i], 2); 25 | } 26 | } 27 | 28 | if (sumOneSquared > 0 && sumTwoSquared > 0) 29 | { 30 | return sumProduct / (Math.Sqrt(sumOneSquared) * Math.Sqrt(sumTwoSquared)); 31 | } 32 | else 33 | { 34 | return double.NegativeInfinity; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /User Behavior/Comparers/CorrelationUserComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Comparers 8 | { 9 | public class CorrelationUserComparer : IComparer 10 | { 11 | public double CompareVectors(double[] userFeaturesOne, double[] userFeaturesTwo) 12 | { 13 | double average1 = 0.0; 14 | double average2 = 0.0; 15 | int count = 0; 16 | 17 | for (int i = 0; i < userFeaturesOne.Length; i++) 18 | { 19 | if (userFeaturesOne[i] != 0 && userFeaturesTwo[i] != 0) 20 | { 21 | average1 += userFeaturesOne[i]; 22 | average2 += userFeaturesTwo[i]; 23 | count++; 24 | } 25 | } 26 | 27 | average1 /= count; 28 | average2 /= count; 29 | 30 | double sum = 0.0; 31 | double squares1 = 0.0; 32 | double squares2 = 0.0; 33 | 34 | for (int i = 0; i < userFeaturesOne.Length; i++) 35 | { 36 | if (userFeaturesOne[i] != 0 && userFeaturesTwo[i] != 0) 37 | { 38 | sum += (userFeaturesOne[i] - average1) * (userFeaturesTwo[i] - average2); 39 | squares1 += Math.Pow(userFeaturesOne[i] - average1, 2); 40 | squares2 += Math.Pow(userFeaturesTwo[i] - average2, 2); 41 | } 42 | } 43 | 44 | return sum / Math.Sqrt(squares1 * squares2); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /User Behavior/Comparers/CosineUserComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Comparers 8 | { 9 | public class CosineUserComparer : IComparer 10 | { 11 | public double CompareVectors(double[] userFeaturesOne, double[] userFeaturesTwo) 12 | { 13 | double sumProduct = 0.0; 14 | double sumOneSquared = 0.0; 15 | double sumTwoSquared = 0.0; 16 | 17 | for (int i = 0; i < userFeaturesOne.Length; i++) 18 | { 19 | sumProduct += userFeaturesOne[i] * userFeaturesTwo[i]; 20 | sumOneSquared += Math.Pow(userFeaturesOne[i], 2); 21 | sumTwoSquared += Math.Pow(userFeaturesTwo[i], 2); 22 | } 23 | 24 | return sumProduct / (Math.Sqrt(sumOneSquared) * Math.Sqrt(sumTwoSquared)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /User Behavior/Comparers/RootMeanSquareUserComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Comparers 8 | { 9 | public class RootMeanSquareUserComparer : IComparer 10 | { 11 | public double CompareVectors(double[] userFeaturesOne, double[] userFeaturesTwo) 12 | { 13 | double score = 0.0; 14 | 15 | for (int i = 0; i < userFeaturesOne.Length; i++) 16 | { 17 | score += Math.Pow(userFeaturesOne[i] - userFeaturesTwo[i], 2); 18 | } 19 | 20 | // Higher numbers indicate closer similarity 21 | return -Math.Sqrt(score / userFeaturesOne.Length); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /User Behavior/Comparers/SimpleCountUserComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Comparers 8 | { 9 | public class SimpleCountUserComparer : IComparer 10 | { 11 | public double CompareVectors(double[] userFeaturesOne, double[] userFeaturesTwo) 12 | { 13 | double count = 0.0; 14 | for (int i = 0; i < userFeaturesOne.Length; i++) 15 | { 16 | if (userFeaturesOne[i] != 0 && userFeaturesTwo[i] != 0) 17 | { 18 | count++; 19 | } 20 | } 21 | return count; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /User Behavior/Mathematics/Matrix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Mathematics 8 | { 9 | class Matrix 10 | { 11 | /// 12 | /// Calculate the dot product between two vectors 13 | /// 14 | public static double GetDotProduct(double[] matrixOne, double[] matrixTwo) 15 | { 16 | return matrixOne.Zip(matrixTwo, (a, b) => a * b).Sum(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /User Behavior/Mathematics/SingularValueDecomposition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using UserBehavior.Objects; 8 | 9 | namespace UserBehavior.Mathematics 10 | { 11 | class SingularValueDecomposition 12 | { 13 | private double averageGlobalRating; 14 | 15 | private int learningIterations; 16 | private double learningRate; 17 | private double learningDescent = 0.99; 18 | private double regularizationTerm = 0.02; 19 | 20 | private int numUsers; 21 | private int numArticles; 22 | private int numFeatures; 23 | 24 | private double[] userBiases; 25 | private double[] articleBiases; 26 | private double[][] userFeatures; 27 | private double[][] articleFeatures; 28 | 29 | public SingularValueDecomposition() 30 | : this(20, 100) 31 | { 32 | } 33 | 34 | public SingularValueDecomposition(int features, int iterations) 35 | : this(features, iterations, 0.005) 36 | { 37 | } 38 | 39 | public SingularValueDecomposition(int features, int iterations, double learningSpeed) 40 | { 41 | numFeatures = features; 42 | learningIterations = iterations; 43 | learningRate = learningSpeed; 44 | } 45 | 46 | private void Initialize(UserArticleRatingsTable ratings) 47 | { 48 | numUsers = ratings.Users.Count; 49 | numArticles = ratings.Users[0].ArticleRatings.Length; 50 | 51 | Random rand = new Random(); 52 | 53 | userFeatures = new double[numUsers][]; 54 | for (int userIndex = 0; userIndex < numUsers; userIndex++) 55 | { 56 | userFeatures[userIndex] = new double[numFeatures]; 57 | 58 | for (int featureIndex = 0; featureIndex < numFeatures; featureIndex++) 59 | { 60 | userFeatures[userIndex][featureIndex] = rand.NextDouble(); 61 | } 62 | } 63 | 64 | articleFeatures = new double[numArticles][]; 65 | for (int articleIndex = 0; articleIndex < numArticles; articleIndex++) 66 | { 67 | articleFeatures[articleIndex] = new double[numFeatures]; 68 | 69 | for (int featureIndex = 0; featureIndex < numFeatures; featureIndex++) 70 | { 71 | articleFeatures[articleIndex][featureIndex] = rand.NextDouble(); 72 | } 73 | } 74 | 75 | userBiases = new double[numUsers]; 76 | articleBiases = new double[numArticles]; 77 | } 78 | 79 | public SvdResult FactorizeMatrix(UserArticleRatingsTable ratings) 80 | { 81 | Initialize(ratings); 82 | 83 | double squaredError; 84 | int count; 85 | List rmseAll = new List(); 86 | 87 | averageGlobalRating = GetAverageRating(ratings); 88 | 89 | for (int i = 0; i < learningIterations; i++) 90 | { 91 | squaredError = 0.0; 92 | count = 0; 93 | 94 | for (int userIndex = 0; userIndex < numUsers; userIndex++) 95 | { 96 | for (int articleIndex = 0; articleIndex < numArticles; articleIndex++) 97 | { 98 | if (ratings.Users[userIndex].ArticleRatings[articleIndex] != 0) 99 | { 100 | double predictedRating = averageGlobalRating + userBiases[userIndex] + articleBiases[articleIndex] + Matrix.GetDotProduct(userFeatures[userIndex], articleFeatures[articleIndex]); 101 | 102 | double error = ratings.Users[userIndex].ArticleRatings[articleIndex] - predictedRating; 103 | 104 | if (double.IsNaN(predictedRating)) 105 | { 106 | throw new Exception("Encountered a non-number while factorizing a matrix! Try decreasing the learning rate."); 107 | } 108 | 109 | squaredError += Math.Pow(error, 2); 110 | count++; 111 | 112 | averageGlobalRating += learningRate * (error - regularizationTerm * averageGlobalRating); 113 | userBiases[userIndex] += learningRate * (error - regularizationTerm * userBiases[userIndex]); 114 | articleBiases[articleIndex] += learningRate * (error - regularizationTerm * articleBiases[articleIndex]); 115 | 116 | for (int featureIndex = 0; featureIndex < numFeatures; featureIndex++) 117 | { 118 | userFeatures[userIndex][featureIndex] += learningRate * (error * articleFeatures[articleIndex][featureIndex] - regularizationTerm * userFeatures[userIndex][featureIndex]); 119 | articleFeatures[articleIndex][featureIndex] += learningRate * (error * userFeatures[userIndex][featureIndex] - regularizationTerm * articleFeatures[articleIndex][featureIndex]); 120 | } 121 | } 122 | } 123 | } 124 | 125 | squaredError = Math.Sqrt(squaredError / count); 126 | rmseAll.Add(squaredError); 127 | 128 | learningRate *= learningDescent; 129 | } 130 | 131 | //using (StreamWriter w = new StreamWriter("rmse.csv")) 132 | //{ 133 | // w.WriteLine("epoc,rmse"); 134 | // for (int i = 0; i < rmseAll.Count; i++) 135 | // { 136 | // w.WriteLine((i + 1) + "," + rmseAll[i]); 137 | // } 138 | //} 139 | 140 | return new SvdResult(averageGlobalRating, userBiases, articleBiases, userFeatures, articleFeatures); 141 | } 142 | 143 | /// 144 | /// Get the average rating of non-zero values across the entire user-article matrix 145 | /// 146 | private double GetAverageRating(UserArticleRatingsTable ratings) 147 | { 148 | double sum = 0.0; 149 | int count = 0; 150 | 151 | for (int userIndex = 0; userIndex < numUsers; userIndex++) 152 | { 153 | for (int articleIndex = 0; articleIndex < numArticles; articleIndex++) 154 | { 155 | // If the given user rated the given item, add it to our average 156 | if (ratings.Users[userIndex].ArticleRatings[articleIndex] != 0) 157 | { 158 | sum += ratings.Users[userIndex].ArticleRatings[articleIndex]; 159 | count++; 160 | } 161 | } 162 | } 163 | 164 | return sum / count; 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /User Behavior/Mathematics/SvdResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Mathematics 8 | { 9 | class SvdResult 10 | { 11 | public double AverageGlobalRating { get; private set; } 12 | public double[] UserBiases { get; private set; } 13 | public double[] ArticleBiases { get; private set; } 14 | public double[][] UserFeatures { get; private set; } 15 | public double[][] ArticleFeatures { get; private set; } 16 | 17 | public SvdResult(double averageGlobalRating, double[] userBiases, double[] articleBiases, double[][] userFeatures, double[][] articleFeatures) 18 | { 19 | AverageGlobalRating = averageGlobalRating; 20 | UserBiases = userBiases; 21 | ArticleBiases = articleBiases; 22 | UserFeatures = userFeatures; 23 | ArticleFeatures = articleFeatures; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /User Behavior/Objects/Article.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace UserBehavior.Objects 6 | { 7 | [Serializable] 8 | public class Article 9 | { 10 | public int ArticleID { get; set; } 11 | 12 | public string Name { get; set; } 13 | 14 | public List Tags { get; set; } 15 | 16 | public Article(int id, string name, List tags) 17 | { 18 | ArticleID = id; 19 | Name = name; 20 | Tags = tags; 21 | } 22 | 23 | public override string ToString() 24 | { 25 | return ArticleID + "," + Name + "," + Tags.Select(x => x.Name).Aggregate((c, n) => c + "," + n); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /User Behavior/Objects/ArticleRating.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Objects 8 | { 9 | public class ArticleRating 10 | { 11 | public int ArticleID { get; set; } 12 | 13 | public double Rating { get; set; } 14 | 15 | public ArticleRating(int articleId, double rating) 16 | { 17 | ArticleID = articleId; 18 | Rating = rating; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /User Behavior/Objects/ArticleTag.cs: -------------------------------------------------------------------------------- 1 | namespace UserBehavior.Objects 2 | { 3 | public class ArticleTag 4 | { 5 | public int ArticleID { get; set; } 6 | 7 | public string TagName { get; set; } 8 | 9 | public ArticleTag(int articleid, string tag) 10 | { 11 | ArticleID = articleid; 12 | TagName = tag; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /User Behavior/Objects/ArticleTagCounts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Objects 8 | { 9 | public class ArticleTagCounts 10 | { 11 | public int ArticleID { get; set; } 12 | 13 | public double[] TagCounts { get; set; } 14 | 15 | public ArticleTagCounts(int articleId, int numTags) 16 | { 17 | ArticleID = articleId; 18 | TagCounts = new double[numTags]; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /User Behavior/Objects/ScoreResults.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Objects 8 | { 9 | public class ScoreResults 10 | { 11 | public double RootMeanSquareDifference { get; set; } 12 | 13 | public ScoreResults(double rmsd) 14 | { 15 | RootMeanSquareDifference = rmsd; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /User Behavior/Objects/Suggestion.cs: -------------------------------------------------------------------------------- 1 | namespace UserBehavior.Objects 2 | { 3 | public class Suggestion 4 | { 5 | public int UserID { get; set; } 6 | 7 | public int ArticleID { get; set; } 8 | 9 | public double Rating { get; set; } 10 | 11 | public Suggestion(int userId, int articleId, double assurance) 12 | { 13 | UserID = userId; 14 | ArticleID = articleId; 15 | Rating = assurance; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /User Behavior/Objects/Tag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserBehavior.Objects 4 | { 5 | [Serializable] 6 | public class Tag 7 | { 8 | public string Name { get; set; } 9 | 10 | public Tag(string name) 11 | { 12 | Name = name; 13 | } 14 | 15 | public override string ToString() 16 | { 17 | return Name; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /User Behavior/Objects/TestResults.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Objects 8 | { 9 | public class TestResults 10 | { 11 | public int TotalUsers { get; set; } 12 | 13 | public int UsersSolved { get; set; } 14 | 15 | public double AverageRecall { get; set; } 16 | 17 | public double AveragePrecision { get; set; } 18 | 19 | public TestResults(int totalUsers, int usersSolved, double averageRecall, double averagePrecision) 20 | { 21 | TotalUsers = totalUsers; 22 | UsersSolved = usersSolved; 23 | AverageRecall = averageRecall; 24 | AveragePrecision = averagePrecision; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /User Behavior/Objects/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserBehavior.Objects 4 | { 5 | [Serializable] 6 | public class User 7 | { 8 | public int UserID { get; set; } 9 | 10 | public string Name { get; set; } 11 | 12 | public User(int id, string name) 13 | { 14 | UserID = id; 15 | Name = name; 16 | } 17 | 18 | public override string ToString() 19 | { 20 | return UserID + "," + Name; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /User Behavior/Objects/UserAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UserBehavior.Objects 4 | { 5 | [Serializable] 6 | public class UserAction 7 | { 8 | public int Day { get; set; } 9 | 10 | public string Action { get; set; } 11 | 12 | public int UserID { get; set; } 13 | 14 | public string UserName { get; set; } 15 | 16 | public int ArticleID { get; set; } 17 | 18 | public string ArticleName { get; set; } 19 | 20 | public UserAction(int day, string action, int userid, string username, int articleid, string articlename) 21 | { 22 | Day = day; 23 | Action = action; 24 | UserID = userid; 25 | UserName = username; 26 | ArticleID = articleid; 27 | ArticleName = articlename; 28 | } 29 | 30 | public override string ToString() 31 | { 32 | return Day + "," + Action + "," + UserID + "," + UserName + "," + ArticleID + "," + ArticleName; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /User Behavior/Objects/UserActionTag.cs: -------------------------------------------------------------------------------- 1 | namespace UserBehavior.Objects 2 | { 3 | public class UserActionTag 4 | { 5 | public int UserID { get; set; } 6 | 7 | public double[] ActionTagData { get; set; } 8 | 9 | public double Score { get; set; } 10 | 11 | public UserActionTag(int userId, int actionTagCount) 12 | { 13 | UserID = userId; 14 | ActionTagData = new double[actionTagCount]; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /User Behavior/Objects/UserArticleRatings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace UserBehavior.Objects 5 | { 6 | public class UserArticleRatings 7 | { 8 | public int UserID { get; set; } 9 | 10 | public double[] ArticleRatings { get; set; } 11 | 12 | public double Score { get; set; } 13 | 14 | public UserArticleRatings(int userId, int articlesCount) 15 | { 16 | UserID = userId; 17 | ArticleRatings = new double[articlesCount]; 18 | } 19 | 20 | public void AppendRatings(double[] ratings) 21 | { 22 | List allRatings = new List(); 23 | 24 | allRatings.AddRange(ArticleRatings); 25 | allRatings.AddRange(ratings); 26 | 27 | ArticleRatings = allRatings.ToArray(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /User Behavior/Objects/UserArticleRatingsTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace UserBehavior.Objects 10 | { 11 | public class UserArticleRatingsTable 12 | { 13 | public List Users { get; set; } 14 | 15 | public List UserIndexToID { get; set; } 16 | 17 | public List ArticleIndexToID { get; set; } 18 | 19 | public UserArticleRatingsTable() 20 | { 21 | Users = new List(); 22 | UserIndexToID = new List(); 23 | ArticleIndexToID = new List(); 24 | } 25 | 26 | public void AppendUserFeatures(double[][] userFeatures) 27 | { 28 | for (int i = 0; i < UserIndexToID.Count; i++) 29 | { 30 | Users[i].AppendRatings(userFeatures[i]); 31 | } 32 | } 33 | 34 | public void AppendArticleFeatures(double[][] articleFeatures) 35 | { 36 | for (int f = 0; f < articleFeatures[0].Length; f++) 37 | { 38 | UserArticleRatings newFeature = new UserArticleRatings(int.MaxValue, ArticleIndexToID.Count); 39 | 40 | for (int a = 0; a < ArticleIndexToID.Count; a++) 41 | { 42 | newFeature.ArticleRatings[a] = articleFeatures[a][f]; 43 | } 44 | 45 | Users.Add(newFeature); 46 | } 47 | } 48 | 49 | internal void AppendArticleFeatures(List articleTags) 50 | { 51 | double[][] features = new double[articleTags.Count][]; 52 | 53 | for (int a = 0; a < articleTags.Count; a++) 54 | { 55 | features[a] = new double[articleTags[a].TagCounts.Length]; 56 | 57 | for (int f = 0; f < articleTags[a].TagCounts.Length; f++) 58 | { 59 | features[a][f] = articleTags[a].TagCounts[f]; 60 | } 61 | } 62 | 63 | AppendArticleFeatures(features); 64 | } 65 | 66 | public void SaveSparcityVisual(string file) 67 | { 68 | double min = Users.Min(x => x.ArticleRatings.Min()); 69 | double max = Users.Max(x => x.ArticleRatings.Max()); 70 | 71 | Bitmap b = new Bitmap(ArticleIndexToID.Count, UserIndexToID.Count); 72 | int numPixels = 0; 73 | 74 | for (int x = 0; x < ArticleIndexToID.Count; x++) 75 | { 76 | for (int y = 0; y < UserIndexToID.Count; y++) 77 | { 78 | //int brightness = 255 - (int)(((UserArticleRatings[y].ArticleRatings[x] - min) / (max - min)) * 255); 79 | //brightness = Math.Max(0, Math.Min(255, brightness)); 80 | 81 | int brightness = Users[y].ArticleRatings[x] == 0 ? 255 : 0; 82 | 83 | Color c = Color.FromArgb(brightness, brightness, brightness); 84 | 85 | b.SetPixel(x, y, c); 86 | 87 | numPixels += Users[y].ArticleRatings[x] != 0 ? 1 : 0; 88 | } 89 | } 90 | 91 | double sparcity = (double)numPixels / (ArticleIndexToID.Count * UserIndexToID.Count); 92 | 93 | b.Save(file); 94 | } 95 | 96 | /// 97 | /// Generate a CSV report of users and how many ratings they've given 98 | /// 99 | public void SaveUserRatingDistribution(string file) 100 | { 101 | int bucketSize = 4; 102 | int maxRatings = Users.Max(x => x.ArticleRatings.Count(y => y != 0)); 103 | List buckets = new List(); 104 | 105 | for (int i = 0; i <= Math.Floor((double)maxRatings / bucketSize); i++) 106 | { 107 | buckets.Add(0); 108 | } 109 | 110 | foreach (UserArticleRatings ratings in Users) 111 | { 112 | buckets[(int)Math.Floor((double)ratings.ArticleRatings.Count(x => x != 0) / bucketSize)]++; 113 | } 114 | 115 | using (StreamWriter w = new StreamWriter(file)) 116 | { 117 | w.WriteLine("numArticlesRead,numUsers"); 118 | 119 | for (int i = 0; i <= Math.Floor((double)maxRatings / bucketSize); i++) 120 | { 121 | w.WriteLine("=\"" + (i * bucketSize) + "-" + (((i + 1) * bucketSize) - 1) + "\"," + buckets[i / bucketSize]); 122 | } 123 | } 124 | } 125 | 126 | /// 127 | /// Generate a CSV report of articles and how many ratings they've gotten 128 | /// 129 | public void SaveArticleRatingDistribution(string file) 130 | { 131 | int bucketSize = 2; 132 | int maxRatings = 3000; 133 | List buckets = new List(); 134 | 135 | for (int i = 0; i <= Math.Floor((double)maxRatings / bucketSize); i++) 136 | { 137 | buckets.Add(0); 138 | } 139 | 140 | for (int i = 0; i < ArticleIndexToID.Count; i ++) 141 | { 142 | int readers = Users.Select(x => x.ArticleRatings[i]).Count(x => x != 0); 143 | buckets[(int)Math.Floor((double)readers / bucketSize)]++; 144 | } 145 | 146 | while (buckets[buckets.Count - 1] == 0) 147 | { 148 | buckets.RemoveAt(buckets.Count - 1); 149 | } 150 | 151 | using (StreamWriter w = new StreamWriter(file)) 152 | { 153 | w.WriteLine("numReaders,numArticles"); 154 | 155 | for (int i = 0; i < buckets.Count; i++) 156 | { 157 | w.WriteLine("=\"" + (i * bucketSize) + "-" + (((i + 1) * bucketSize) - 1) + "\"," + buckets[i]); 158 | } 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /User Behavior/Parsers/DaySplitter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UserBehavior.Parsers 8 | { 9 | public class DaySplitter : ISplitter 10 | { 11 | public UserBehaviorDatabase TrainingDB { get; private set; } 12 | 13 | public UserBehaviorDatabase TestingDB { get; private set; } 14 | 15 | public DaySplitter(UserBehaviorDatabase db, int daysForTesting) 16 | { 17 | int splitDay = db.UserActions.Max(x => x.Day) - daysForTesting; 18 | 19 | TrainingDB = db.Clone(); 20 | TrainingDB.UserActions.RemoveAll(x => x.Day > splitDay); 21 | 22 | TestingDB = db.Clone(); 23 | TestingDB.UserActions.RemoveAll(x => x.Day <= splitDay); 24 | 25 | // Remove any user-article pairs from the testing set that are also in the training set 26 | TestingDB.UserActions.RemoveAll(test => TrainingDB.UserActions.Any(train => train.UserID == test.UserID && train.ArticleID == test.ArticleID)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /User Behavior/Parsers/UserBehaviorDatabase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Runtime.Serialization.Formatters.Binary; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using UserBehavior.Objects; 9 | 10 | namespace UserBehavior.Parsers 11 | { 12 | [Serializable] 13 | public class UserBehaviorDatabase 14 | { 15 | public List Tags { get; set; } 16 | 17 | public List
Articles { get; set; } 18 | 19 | public List Users { get; set; } 20 | 21 | public List UserActions { get; set; } 22 | 23 | public UserBehaviorDatabase() 24 | { 25 | Tags = new List(); 26 | Articles = new List
(); 27 | Users = new List(); 28 | UserActions = new List(); 29 | } 30 | 31 | public List GetArticleTagLinkingTable() 32 | { 33 | List articleTags = new List(); 34 | 35 | foreach (Article article in Articles) 36 | { 37 | foreach (Tag tag in article.Tags) 38 | { 39 | articleTags.Add(new ArticleTag(article.ArticleID, tag.Name)); 40 | } 41 | } 42 | 43 | return articleTags; 44 | } 45 | 46 | public UserBehaviorDatabase Clone() 47 | { 48 | using (MemoryStream ms = new MemoryStream()) 49 | { 50 | BinaryFormatter formatter = new BinaryFormatter(); 51 | formatter.Serialize(ms, this); 52 | ms.Position = 0; 53 | return (UserBehaviorDatabase)formatter.Deserialize(ms); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /User Behavior/Parsers/UserBehaviorDatabaseParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using UserBehavior.Objects; 8 | 9 | namespace UserBehavior.Parsers 10 | { 11 | public class UserBehaviorDatabaseParser 12 | { 13 | private const string HeaderTags = "# Tags"; 14 | private const string HeaderArticles = "# Articles"; 15 | private const string HeaderUsers = "# Users"; 16 | private const string HeaderUserActions = "# User actions"; 17 | 18 | public UserBehaviorDatabaseParser() 19 | { 20 | 21 | } 22 | 23 | public UserBehaviorDatabase LoadUserBehaviorDatabase(string file) 24 | { 25 | UserBehaviorDatabase db = new UserBehaviorDatabase(); 26 | 27 | List lines = File.ReadAllLines(file).ToList(); 28 | 29 | // Get the indexes of each data section 30 | int tagsIndex = lines.FindIndex(x => x.StartsWith(HeaderTags)); 31 | int articlesIndex = lines.FindIndex(x => x.StartsWith(HeaderArticles)); 32 | int usersIndex = lines.FindIndex(x => x.StartsWith(HeaderUsers)); 33 | int userActionsIndex = lines.FindIndex(x => x.StartsWith(HeaderUserActions)); 34 | 35 | // Parse out the tags 36 | for (int i = tagsIndex; i < articlesIndex; i++) 37 | { 38 | if (!lines[i].Trim().StartsWith("#") && lines[i].Trim().Length > 0) 39 | { 40 | foreach (string s in lines[i].Split(',')) 41 | { 42 | db.Tags.Add(new Tag(s.Trim())); 43 | } 44 | } 45 | } 46 | 47 | // Parse out the articles 48 | for (int i = articlesIndex; i < usersIndex; i++) 49 | { 50 | if (!lines[i].Trim().StartsWith("#") && lines[i].Trim().Length > 0) 51 | { 52 | string[] cols = lines[i].Split(','); 53 | List tags = new List(); 54 | 55 | for (int c = 2; c < cols.Length; c++) 56 | { 57 | tags.Add(new Tag(cols[c].Trim())); 58 | } 59 | 60 | db.Articles.Add(new Article(int.Parse(cols[0].Trim()), cols[1].Trim(), tags)); 61 | } 62 | } 63 | 64 | // Parse out the users 65 | for (int i = usersIndex; i < userActionsIndex; i++) 66 | { 67 | if (!lines[i].Trim().StartsWith("#") && lines[i].Trim().Length > 0) 68 | { 69 | string[] cols = lines[i].Split(','); 70 | db.Users.Add(new User(int.Parse(cols[0].Trim()), cols[1].Trim())); 71 | } 72 | } 73 | 74 | // Parse out the user actions 75 | for (int i = userActionsIndex; i < lines.Count; i++) 76 | { 77 | if (!lines[i].Trim().StartsWith("#") && lines[i].Trim().Length > 0) 78 | { 79 | string[] cols = lines[i].Split(','); 80 | db.UserActions.Add(new UserAction(int.Parse(cols[0].Trim()), cols[1].Trim(), int.Parse(cols[2].Trim()), cols[3].Trim(), int.Parse(cols[4].Trim()), cols[5].Trim())); 81 | } 82 | } 83 | 84 | return db; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /User Behavior/Parsers/UserBehaviorTransformer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using UserBehavior.Abstractions; 8 | using UserBehavior.Objects; 9 | 10 | namespace UserBehavior.Parsers 11 | { 12 | public class UserBehaviorTransformer 13 | { 14 | private UserBehaviorDatabase db; 15 | 16 | public UserBehaviorTransformer(UserBehaviorDatabase database) 17 | { 18 | db = database; 19 | } 20 | 21 | /// 22 | /// Get a list of all users and their ratings on every article 23 | /// 24 | public UserArticleRatingsTable GetUserArticleRatingsTable(IRater rater) 25 | { 26 | UserArticleRatingsTable table = new UserArticleRatingsTable(); 27 | 28 | table.UserIndexToID = db.Users.OrderBy(x => x.UserID).Select(x => x.UserID).Distinct().ToList(); 29 | table.ArticleIndexToID = db.Articles.OrderBy(x => x.ArticleID).Select(x => x.ArticleID).Distinct().ToList(); 30 | 31 | foreach (int userId in table.UserIndexToID) 32 | { 33 | table.Users.Add(new UserArticleRatings(userId, table.ArticleIndexToID.Count)); 34 | } 35 | 36 | var userArticleRatingGroup = db.UserActions 37 | .GroupBy(x => new { x.UserID, x.ArticleID }) 38 | .Select(g => new { g.Key.UserID, g.Key.ArticleID, Rating = rater.GetRating(g.ToList()) }) 39 | .ToList(); 40 | 41 | foreach (var userAction in userArticleRatingGroup) 42 | { 43 | int userIndex = table.UserIndexToID.IndexOf(userAction.UserID); 44 | int articleIndex = table.ArticleIndexToID.IndexOf(userAction.ArticleID); 45 | 46 | table.Users[userIndex].ArticleRatings[articleIndex] = userAction.Rating; 47 | } 48 | 49 | return table; 50 | } 51 | 52 | /// 53 | /// Get a table of all articles as rows and all tags as columns 54 | /// 55 | public List GetArticleTagCounts() 56 | { 57 | List articleTags = new List(); 58 | 59 | foreach (Article article in db.Articles) 60 | { 61 | ArticleTagCounts articleTag = new ArticleTagCounts(article.ArticleID, db.Tags.Count); 62 | 63 | for (int tag = 0; tag < db.Tags.Count; tag++) 64 | { 65 | articleTag.TagCounts[tag] = article.Tags.Any(x => x.Name == db.Tags[tag].Name) ? 1.0 : 0.0; 66 | } 67 | 68 | articleTags.Add(articleTag); 69 | } 70 | 71 | return articleTags; 72 | } 73 | 74 | /// 75 | /// Get a list of all users and the number of times they viewed articles with a specific tag 76 | /// 77 | [Obsolete] 78 | public List GetUserTags() 79 | { 80 | List userData = new List(); 81 | List articleTags = db.GetArticleTagLinkingTable(); 82 | 83 | int numFeatures = db.Tags.Count; 84 | 85 | // Create a dataset that links every user action to a list of tags associated with the article that action was for, then 86 | // group them by user, action, and tag so we can get a count of the number of times each user performed a action on an article with a specific tag 87 | var userActionTags = db.UserActions 88 | .Join(articleTags, u => u.ArticleID, t => t.ArticleID, (u, t) => new { u.UserID, t.TagName }) 89 | .GroupBy(x => new { x.UserID, x.TagName }) 90 | .Select(g => new { g.Key.UserID, g.Key.TagName, Count = g.Count() }) 91 | .OrderBy(x => x.UserID).ThenBy(x => x.TagName) 92 | .ToList(); 93 | 94 | int totalUserActions = userActionTags.Count(); 95 | int lastFoundIndex = 0; 96 | 97 | // Get action-tag data 98 | foreach (User user in db.Users) 99 | { 100 | int dataCol = 0; 101 | UserActionTag uat = new UserActionTag(user.UserID, numFeatures); 102 | 103 | foreach (Tag tag in db.Tags) 104 | { 105 | // Count the number of times this user interacted with an article with this tag 106 | // We can loop through like this since the list is sorted 107 | int tagActionCount = 0; 108 | for (int i = lastFoundIndex; i < totalUserActions; i++) 109 | { 110 | if (userActionTags[i].UserID == user.UserID && userActionTags[i].TagName == tag.Name) 111 | { 112 | lastFoundIndex = i; 113 | tagActionCount = userActionTags[i].Count; 114 | break; 115 | } 116 | } 117 | 118 | uat.ActionTagData[dataCol++] = tagActionCount; 119 | } 120 | 121 | // Normalize data to values between 0 and 5 122 | double upperLimit = 5.0; 123 | double max = uat.ActionTagData.Max(); 124 | if (max > 0) 125 | { 126 | for (int i = 0; i < uat.ActionTagData.Length; i++) 127 | { 128 | uat.ActionTagData[i] = (uat.ActionTagData[i] / max) * upperLimit; 129 | } 130 | } 131 | 132 | userData.Add(uat); 133 | } 134 | 135 | return userData; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /User Behavior/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("User Behavior")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Scott Clayton")] 12 | [assembly: AssemblyProduct("User Behavior")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("db5a3fef-d18f-4901-9b4f-959e403bd21b")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /User Behavior/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace UserBehavior.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("UserBehavior.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /User Behavior/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /User Behavior/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace UserBehavior.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /User Behavior/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /User Behavior/Raters/LinearRater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using UserBehavior.Abstractions; 7 | using UserBehavior.Objects; 8 | 9 | namespace UserBehavior.Raters 10 | { 11 | public class LinearRater : IRater 12 | { 13 | private double downVoteWeight; 14 | private double upVoteWeight; 15 | private double viewWeight; 16 | private double downloadWeight; 17 | 18 | private double minWeight; 19 | private double maxWeight; 20 | 21 | public LinearRater() 22 | : this (-5.0, 1.0, 3.0, 0.5, 5.0) 23 | { 24 | } 25 | 26 | public LinearRater(double downVote, double upvote, double view, double download) 27 | : this (downVote, upvote, view, download, 5.0) 28 | { 29 | } 30 | 31 | public LinearRater(double downVote, double upVote, double view, double download, double max) 32 | { 33 | downVoteWeight = downVote; 34 | upVoteWeight = upVote; 35 | viewWeight = view; 36 | downloadWeight = download; 37 | 38 | minWeight = 0.1; 39 | maxWeight = max; 40 | } 41 | 42 | public double GetRating(List actions) 43 | { 44 | int up = actions.Count(x => x.Action == "UpVote"); 45 | int down = actions.Count(x => x.Action == "DownVote"); 46 | int view = actions.Count(x => x.Action == "View"); 47 | int dl = actions.Count(x => x.Action == "Download"); 48 | 49 | double rating = up * upVoteWeight + down * downVoteWeight + view * viewWeight + dl * downloadWeight; 50 | 51 | return Math.Min(maxWeight, Math.Max(minWeight, rating)); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /User Behavior/Raters/SimpleRater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using UserBehavior.Abstractions; 7 | using UserBehavior.Objects; 8 | 9 | namespace UserBehavior.Raters 10 | { 11 | /// 12 | /// Return 0 if the article was downvoted, otherwise provide a rating of 1. 13 | /// 14 | public class SimpleRater : IRater 15 | { 16 | public double GetRating(List actions) 17 | { 18 | if (actions.Any(x => x.Action == "DownVote")) 19 | { 20 | return 0.0; 21 | } 22 | 23 | return 1.0; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /User Behavior/Raters/WeightedRater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using UserBehavior.Abstractions; 7 | using UserBehavior.Objects; 8 | 9 | namespace UserBehavior.Raters 10 | { 11 | public class WeightedRater : IRater 12 | { 13 | private double downVoteWeight; 14 | private double baseWeight; 15 | private double upVoteWeight; 16 | private double viewWeight; 17 | private double downloadWeight; 18 | private double maxWeight; 19 | 20 | public WeightedRater() 21 | : this (0.1, 3.0, 1.0, 0.5, 0.5, 5.0) 22 | { 23 | } 24 | 25 | public WeightedRater(double downVote, double baseVal, double upvote, double view, double download) 26 | : this (downVote, baseVal, upvote, view, download, 5.0) 27 | { 28 | } 29 | 30 | public WeightedRater(double downVote, double baseVal, double upvote, double view, double download, double max) 31 | { 32 | downVoteWeight = downVote; 33 | baseWeight = baseVal; 34 | upVoteWeight = upvote; 35 | viewWeight = view; 36 | downloadWeight = download; 37 | maxWeight = max; 38 | } 39 | 40 | public double GetRating(List actions) 41 | { 42 | double rating; 43 | string lastVote = actions.LastOrDefault(x => x.Action == "DownVote" || x.Action == "UpVote")?.Action ?? ""; 44 | int viewCount = actions.Count(x => x.Action == "View"); 45 | bool downloaded = actions.Any(x => x.Action == "Download"); 46 | 47 | if (lastVote == "DownVote") 48 | { 49 | rating = downVoteWeight; 50 | } 51 | else 52 | { 53 | rating = baseWeight; 54 | 55 | rating += lastVote == "UpVote" ? upVoteWeight : 0.0; 56 | rating += viewCount * viewWeight; 57 | rating += downloaded ? downloadWeight : 0.0; 58 | } 59 | 60 | return Math.Min(maxWeight, Math.Max(0.0, rating)); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /User Behavior/Recommender.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {DB5A3FEF-D18F-4901-9B4F-959E403BD21B} 8 | Library 9 | Properties 10 | UserBehavior 11 | ScottClayton.Recommendation 12 | v4.5.2 13 | 512 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | ResXFileCodeGenerator 95 | Resources.Designer.cs 96 | Designer 97 | 98 | 99 | True 100 | Resources.resx 101 | True 102 | 103 | 104 | SettingsSingleFileGenerator 105 | Settings.Designer.cs 106 | 107 | 108 | True 109 | Settings.settings 110 | True 111 | 112 | 113 | 114 | 121 | -------------------------------------------------------------------------------- /User Behavior/Recommenders/ClassifierExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using UserBehavior.Abstractions; 7 | using UserBehavior.Objects; 8 | using UserBehavior.Parsers; 9 | using UserBehavior.Raters; 10 | 11 | namespace UserBehavior.Recommenders 12 | { 13 | public static class ClassifierExtensions 14 | { 15 | public static TestResults Test(this IRecommender classifier, UserBehaviorDatabase db, int numSuggestions) 16 | { 17 | // We're only using the ratings to check for existence of a rating, so we can use a simple rater for everything 18 | SimpleRater rater = new SimpleRater(); 19 | UserBehaviorTransformer ubt = new UserBehaviorTransformer(db); 20 | UserArticleRatingsTable ratings = ubt.GetUserArticleRatingsTable(rater); 21 | 22 | int correctUsers = 0; 23 | double averagePrecision = 0.0; 24 | double averageRecall = 0.0; 25 | 26 | // Get a list of users in this database who interacted with an article for the first time 27 | List distinctUsers = db.UserActions.Select(x => x.UserID).Distinct().ToList(); 28 | 29 | var distinctUserArticles = db.UserActions.GroupBy(x => new { x.UserID, x.ArticleID }); 30 | 31 | // Now get suggestions for each of these users 32 | foreach (int user in distinctUsers) 33 | { 34 | List suggestions = classifier.GetSuggestions(user, numSuggestions); 35 | bool foundOne = false; 36 | int userIndex = ratings.UserIndexToID.IndexOf(user); 37 | 38 | int userCorrectArticles = 0; 39 | int userTotalArticles = distinctUserArticles.Count(x => x.Key.UserID == user); 40 | 41 | foreach (Suggestion s in suggestions) 42 | { 43 | int articleIndex = ratings.ArticleIndexToID.IndexOf(s.ArticleID); 44 | 45 | // If one of the top N suggestions is what the user ended up reading, then we're golden 46 | if (ratings.Users[userIndex].ArticleRatings[articleIndex] != 0) 47 | { 48 | userCorrectArticles++; 49 | 50 | if (!foundOne) 51 | { 52 | correctUsers++; 53 | foundOne = true; 54 | } 55 | } 56 | } 57 | 58 | averagePrecision += (double)userCorrectArticles / numSuggestions; 59 | averageRecall += (double)userCorrectArticles / userTotalArticles; 60 | } 61 | 62 | averagePrecision /= distinctUsers.Count; 63 | averageRecall /= distinctUsers.Count; 64 | 65 | return new TestResults(distinctUsers.Count, correctUsers, averageRecall, averagePrecision); 66 | } 67 | 68 | public static ScoreResults Score(this IRecommender classifier, UserBehaviorDatabase db, IRater rater) 69 | { 70 | UserBehaviorTransformer ubt = new UserBehaviorTransformer(db); 71 | UserArticleRatingsTable actualRatings = ubt.GetUserArticleRatingsTable(rater); 72 | 73 | var distinctUserArticlePairs = db.UserActions.GroupBy(x => new { x.UserID, x.ArticleID }).ToList(); 74 | 75 | double score = 0.0; 76 | int count = 0; 77 | 78 | foreach (var userArticle in distinctUserArticlePairs) 79 | { 80 | int userIndex = actualRatings.UserIndexToID.IndexOf(userArticle.Key.UserID); 81 | int articleIndex = actualRatings.ArticleIndexToID.IndexOf(userArticle.Key.ArticleID); 82 | 83 | double actualRating = actualRatings.Users[userIndex].ArticleRatings[articleIndex]; 84 | 85 | if (actualRating != 0) 86 | { 87 | double predictedRating = classifier.GetRating(userArticle.Key.UserID, userArticle.Key.ArticleID); 88 | 89 | score += Math.Pow(predictedRating - actualRating, 2); 90 | count++; 91 | } 92 | } 93 | 94 | if (count > 0) 95 | { 96 | score = Math.Sqrt(score / count); 97 | } 98 | 99 | return new ScoreResults(score); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /User Behavior/Recommenders/HybridRecommender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using UserBehavior.Objects; 8 | using UserBehavior.Parsers; 9 | 10 | namespace UserBehavior.Recommenders 11 | { 12 | public class HybridRecommender : IRecommender 13 | { 14 | private List classifiers; 15 | 16 | public HybridRecommender() 17 | { 18 | classifiers = new List(); 19 | } 20 | 21 | public HybridRecommender(params IRecommender[] recommenders) 22 | { 23 | classifiers = recommenders.ToList(); 24 | } 25 | 26 | public void Add(IRecommender classifier) 27 | { 28 | classifiers.Add(classifier); 29 | } 30 | 31 | public void Train(UserBehaviorDatabase db) 32 | { 33 | foreach (IRecommender classifier in classifiers) 34 | { 35 | classifier.Train(db); 36 | } 37 | } 38 | 39 | public double GetRating(int userId, int articleId) 40 | { 41 | return classifiers.Select(classifier => classifier.GetRating(userId, articleId)).Average(); 42 | } 43 | 44 | public List GetSuggestions(int userId, int numSuggestions) 45 | { 46 | List suggestions = new List(); 47 | int numSuggestionsEach = (int)Math.Ceiling((double)numSuggestions / classifiers.Count); 48 | 49 | foreach (IRecommender classifier in classifiers) 50 | { 51 | suggestions.AddRange(classifier.GetSuggestions(userId, numSuggestionsEach)); 52 | } 53 | 54 | suggestions.Sort((c, n) => n.Rating.CompareTo(c.Rating)); 55 | 56 | return suggestions.Take(numSuggestions).ToList(); 57 | } 58 | 59 | private List GetCommonSuggestions(int userId, int numSuggestions) 60 | { 61 | int internalSuggestions = 100; 62 | 63 | List> suggestions = new List>(); 64 | foreach (IRecommender classifier in classifiers) 65 | { 66 | suggestions.Add(classifier.GetSuggestions(userId, internalSuggestions)); 67 | } 68 | 69 | List> final = new List>(); 70 | 71 | foreach (List list in suggestions) 72 | { 73 | foreach (Suggestion suggestion in list) 74 | { 75 | int existingIndex = final.FindIndex(x => x.Item1.ArticleID == suggestion.ArticleID); 76 | 77 | if (existingIndex >= 0) 78 | { 79 | Suggestion highestRated = final[existingIndex].Item1.Rating > suggestion.Rating ? final[existingIndex].Item1 : suggestion; 80 | final[existingIndex] = new Tuple(highestRated, final[existingIndex].Item2 + 1); 81 | } 82 | else 83 | { 84 | final.Add(new Tuple(suggestion, 1)); 85 | } 86 | } 87 | } 88 | 89 | final = final.OrderByDescending(x => x.Item2).ThenByDescending(x => x.Item1.Rating).ToList(); 90 | 91 | return final.Select(x => x.Item1).Take(numSuggestions).ToList(); 92 | } 93 | 94 | public void Save(string file) 95 | { 96 | int n = 1; 97 | foreach (IRecommender classifier in classifiers) 98 | { 99 | classifier.Save(file + ".rm" + (n++)); 100 | } 101 | } 102 | 103 | public void Load(string file) 104 | { 105 | int n = 1; 106 | foreach (IRecommender classifier in classifiers) 107 | { 108 | classifier.Load(file + ".rm" + (n++)); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /User Behavior/Recommenders/ItemCollaborativeFilterRecommender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using UserBehavior.Abstractions; 9 | using UserBehavior.Comparers; 10 | using UserBehavior.Mathematics; 11 | using UserBehavior.Objects; 12 | using UserBehavior.Parsers; 13 | 14 | namespace UserBehavior.Recommenders 15 | { 16 | public class ItemCollaborativeFilterRecommender : IRecommender 17 | { 18 | private IComparer comparer; 19 | private IRater rater; 20 | private UserArticleRatingsTable ratings; 21 | private double[][] transposedRatings; 22 | 23 | private int neighborCount; 24 | 25 | public ItemCollaborativeFilterRecommender(IComparer itemComparer, IRater implicitRater, int numberOfNeighbors) 26 | { 27 | comparer = itemComparer; 28 | rater = implicitRater; 29 | neighborCount = numberOfNeighbors; 30 | } 31 | 32 | private void FillTransposedRatings() 33 | { 34 | int features = ratings.Users.Count; 35 | transposedRatings = new double[ratings.ArticleIndexToID.Count][]; 36 | 37 | // Precompute a transposed ratings matrix where each row becomes an article and each column a user or tag 38 | for (int a = 0; a < ratings.ArticleIndexToID.Count; a++) 39 | { 40 | transposedRatings[a] = new double[features]; 41 | 42 | for (int f = 0; f < features; f++) 43 | { 44 | transposedRatings[a][f] = ratings.Users[f].ArticleRatings[a]; 45 | } 46 | } 47 | } 48 | 49 | private List GetHighestRatedArticlesForUser(int userIndex) 50 | { 51 | List> items = new List>(); 52 | 53 | for (int articleIndex = 0; articleIndex < ratings.ArticleIndexToID.Count; articleIndex++) 54 | { 55 | // Create a list of every article this user has viewed 56 | if (ratings.Users[userIndex].ArticleRatings[articleIndex] != 0) 57 | { 58 | items.Add(new Tuple(articleIndex, ratings.Users[userIndex].ArticleRatings[articleIndex])); 59 | } 60 | } 61 | 62 | // Sort the articles by rating 63 | items.Sort((c, n) => n.Item2.CompareTo(c.Item2)); 64 | 65 | return items.Select(x => x.Item1).ToList(); 66 | } 67 | 68 | public void Train(UserBehaviorDatabase db) 69 | { 70 | UserBehaviorTransformer ubt = new UserBehaviorTransformer(db); 71 | ratings = ubt.GetUserArticleRatingsTable(rater); 72 | 73 | List articleTags = ubt.GetArticleTagCounts(); 74 | ratings.AppendArticleFeatures(articleTags); 75 | 76 | FillTransposedRatings(); 77 | } 78 | 79 | public double GetRating(int userId, int articleId) 80 | { 81 | int userIndex = ratings.UserIndexToID.IndexOf(userId); 82 | int articleIndex = ratings.ArticleIndexToID.IndexOf(articleId); 83 | 84 | var userRatings = ratings.Users[userIndex].ArticleRatings.Where(x => x != 0); 85 | var articleRatings = ratings.Users.Select(x => x.ArticleRatings[articleIndex]).Where(x => x != 0); 86 | 87 | double averageUser = userRatings.Count() > 0 ? userRatings.Average() : 0; 88 | double averageArticle = articleRatings.Count() > 0 ? articleRatings.Average() : 0; 89 | 90 | // For now, just return the average rating across this user and article 91 | return averageArticle > 0 && averageUser > 0 ? (averageUser + averageArticle) / 2.0 : Math.Max(averageUser, averageArticle); 92 | } 93 | 94 | public List GetSuggestions(int userId, int numSuggestions) 95 | { 96 | int userIndex = ratings.UserIndexToID.IndexOf(userId); 97 | List articles = GetHighestRatedArticlesForUser(userIndex).Take(5).ToList(); 98 | List suggestions = new List(); 99 | 100 | foreach (int articleIndex in articles) 101 | { 102 | int articleId = ratings.ArticleIndexToID[articleIndex]; 103 | List neighboringArticles = GetNearestNeighbors(articleId, neighborCount); 104 | 105 | foreach (ArticleRating neighbor in neighboringArticles) 106 | { 107 | int neighborArticleIndex = ratings.ArticleIndexToID.IndexOf(neighbor.ArticleID); 108 | 109 | double averageArticleRating = 0.0; 110 | int count = 0; 111 | for (int userRatingIndex = 0; userRatingIndex < ratings.UserIndexToID.Count; userRatingIndex++) 112 | { 113 | if (transposedRatings[neighborArticleIndex][userRatingIndex] != 0) 114 | { 115 | averageArticleRating += transposedRatings[neighborArticleIndex][userRatingIndex]; 116 | count++; 117 | } 118 | } 119 | if (count > 0) 120 | { 121 | averageArticleRating /= count; 122 | } 123 | 124 | suggestions.Add(new Suggestion(userId, neighbor.ArticleID, averageArticleRating)); 125 | } 126 | } 127 | 128 | suggestions.Sort((c, n) => n.Rating.CompareTo(c.Rating)); 129 | 130 | return suggestions.Take(numSuggestions).ToList(); 131 | } 132 | 133 | private List GetNearestNeighbors(int articleId, int numArticles) 134 | { 135 | List neighbors = new List(); 136 | int mainArticleIndex = ratings.ArticleIndexToID.IndexOf(articleId); 137 | 138 | for (int articleIndex = 0; articleIndex < ratings.ArticleIndexToID.Count; articleIndex++) 139 | { 140 | int searchArticleId = ratings.ArticleIndexToID[articleIndex]; 141 | 142 | double score = comparer.CompareVectors(transposedRatings[mainArticleIndex], transposedRatings[articleIndex]); 143 | 144 | neighbors.Add(new ArticleRating(searchArticleId, score)); 145 | } 146 | 147 | neighbors.Sort((c, n) => n.Rating.CompareTo(c.Rating)); 148 | 149 | return neighbors.Take(numArticles).ToList(); 150 | } 151 | 152 | public void Save(string file) 153 | { 154 | using (FileStream fs = new FileStream(file, FileMode.Create)) 155 | using (GZipStream zip = new GZipStream(fs, CompressionMode.Compress)) 156 | using (StreamWriter w = new StreamWriter(zip)) 157 | { 158 | w.WriteLine(ratings.Users.Count); 159 | w.WriteLine(ratings.Users[0].ArticleRatings.Length); 160 | 161 | foreach (UserArticleRatings t in ratings.Users) 162 | { 163 | w.WriteLine(t.UserID); 164 | 165 | foreach (double v in t.ArticleRatings) 166 | { 167 | w.WriteLine(v); 168 | } 169 | } 170 | 171 | w.WriteLine(ratings.UserIndexToID.Count); 172 | 173 | foreach (int i in ratings.UserIndexToID) 174 | { 175 | w.WriteLine(i); 176 | } 177 | 178 | w.WriteLine(ratings.ArticleIndexToID.Count); 179 | 180 | foreach (int i in ratings.ArticleIndexToID) 181 | { 182 | w.WriteLine(i); 183 | } 184 | } 185 | } 186 | 187 | public void Load(string file) 188 | { 189 | ratings = new UserArticleRatingsTable(); 190 | 191 | using (FileStream fs = new FileStream(file, FileMode.Open)) 192 | using (GZipStream zip = new GZipStream(fs, CompressionMode.Decompress)) 193 | using (StreamReader r = new StreamReader(zip)) 194 | { 195 | long total = long.Parse(r.ReadLine()); 196 | int features = int.Parse(r.ReadLine()); 197 | 198 | for (long i = 0; i < total; i++) 199 | { 200 | int userId = int.Parse(r.ReadLine()); 201 | UserArticleRatings uat = new UserArticleRatings(userId, features); 202 | 203 | for (int x = 0; x < features; x++) 204 | { 205 | uat.ArticleRatings[x] = double.Parse(r.ReadLine()); 206 | } 207 | 208 | ratings.Users.Add(uat); 209 | } 210 | 211 | total = int.Parse(r.ReadLine()); 212 | 213 | for (int i = 0; i < total; i++) 214 | { 215 | ratings.UserIndexToID.Add(int.Parse(r.ReadLine())); 216 | } 217 | 218 | total = int.Parse(r.ReadLine()); 219 | 220 | for (int i = 0; i < total; i++) 221 | { 222 | ratings.ArticleIndexToID.Add(int.Parse(r.ReadLine())); 223 | } 224 | } 225 | 226 | FillTransposedRatings(); 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /User Behavior/Recommenders/MatrixFactorizationRecommender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using UserBehavior.Abstractions; 9 | using UserBehavior.Mathematics; 10 | using UserBehavior.Objects; 11 | using UserBehavior.Parsers; 12 | 13 | namespace UserBehavior.Recommenders 14 | { 15 | public class MatrixFactorizationRecommender : IRecommender 16 | { 17 | private UserArticleRatingsTable ratings; 18 | private SvdResult svd; 19 | private IRater rater; 20 | 21 | private int numUsers; 22 | private int numArticles; 23 | 24 | private int numFeatures; 25 | private int learningIterations; 26 | 27 | public MatrixFactorizationRecommender(IRater implicitRater) 28 | : this(20, implicitRater) 29 | { 30 | } 31 | 32 | public MatrixFactorizationRecommender(int features, IRater implicitRater) 33 | { 34 | numFeatures = features; 35 | learningIterations = 100; 36 | rater = implicitRater; 37 | } 38 | 39 | public void Train(UserBehaviorDatabase db) 40 | { 41 | UserBehaviorTransformer ubt = new UserBehaviorTransformer(db); 42 | ratings = ubt.GetUserArticleRatingsTable(rater); 43 | 44 | SingularValueDecomposition factorizer = new SingularValueDecomposition(numFeatures, learningIterations); 45 | svd = factorizer.FactorizeMatrix(ratings); 46 | 47 | numUsers = ratings.UserIndexToID.Count; 48 | numArticles = ratings.ArticleIndexToID.Count; 49 | } 50 | 51 | public double GetRating(int userId, int articleId) 52 | { 53 | int userIndex = ratings.UserIndexToID.IndexOf(userId); 54 | int articleIndex = ratings.ArticleIndexToID.IndexOf(articleId); 55 | 56 | return GetRatingForIndex(userIndex, articleIndex); 57 | } 58 | 59 | private double GetRatingForIndex(int userIndex, int articleIndex) 60 | { 61 | return svd.AverageGlobalRating + svd.UserBiases[userIndex] + svd.ArticleBiases[articleIndex] + Matrix.GetDotProduct(svd.UserFeatures[userIndex], svd.ArticleFeatures[articleIndex]); 62 | } 63 | 64 | public List GetSuggestions(int userId, int numSuggestions) 65 | { 66 | int userIndex = ratings.UserIndexToID.IndexOf(userId); 67 | UserArticleRatings user = ratings.Users[userIndex]; 68 | List suggestions = new List(); 69 | 70 | for (int articleIndex = 0; articleIndex < ratings.ArticleIndexToID.Count; articleIndex++) 71 | { 72 | // If the user in question hasn't rated the given article yet 73 | if (user.ArticleRatings[articleIndex] == 0) 74 | { 75 | double rating = GetRatingForIndex(userIndex, articleIndex); 76 | 77 | suggestions.Add(new Suggestion(userId, ratings.ArticleIndexToID[articleIndex], rating)); 78 | } 79 | } 80 | 81 | suggestions.Sort((c, n) => n.Rating.CompareTo(c.Rating)); 82 | 83 | return suggestions.Take(numSuggestions).ToList(); 84 | } 85 | 86 | public void Save(string file) 87 | { 88 | using (FileStream fs = new FileStream(file, FileMode.Create)) 89 | using (GZipStream zip = new GZipStream(fs, CompressionMode.Compress)) 90 | using (StreamWriter w = new StreamWriter(zip)) 91 | { 92 | w.WriteLine(numUsers); 93 | w.WriteLine(numArticles); 94 | w.WriteLine(numFeatures); 95 | 96 | w.WriteLine(svd.AverageGlobalRating); 97 | 98 | for (int userIndex = 0; userIndex < numUsers; userIndex++) 99 | { 100 | w.WriteLine(svd.UserBiases[userIndex]); 101 | } 102 | 103 | for (int articleIndex = 0; articleIndex < numArticles; articleIndex++) 104 | { 105 | w.WriteLine(svd.ArticleBiases[articleIndex]); 106 | } 107 | 108 | for (int userIndex = 0; userIndex < numUsers; userIndex++) 109 | { 110 | for (int featureIndex = 0; featureIndex < numFeatures; featureIndex++) 111 | { 112 | w.WriteLine(svd.UserFeatures[userIndex][featureIndex]); 113 | } 114 | } 115 | 116 | for (int articleIndex = 0; articleIndex < numUsers; articleIndex++) 117 | { 118 | for (int featureIndex = 0; featureIndex < numFeatures; featureIndex++) 119 | { 120 | w.WriteLine(svd.ArticleFeatures[articleIndex][featureIndex]); 121 | } 122 | } 123 | 124 | foreach (UserArticleRatings t in ratings.Users) 125 | { 126 | w.WriteLine(t.UserID); 127 | 128 | foreach (double v in t.ArticleRatings) 129 | { 130 | w.WriteLine(v); 131 | } 132 | } 133 | 134 | foreach (int i in ratings.UserIndexToID) 135 | { 136 | w.WriteLine(i); 137 | } 138 | 139 | foreach (int i in ratings.ArticleIndexToID) 140 | { 141 | w.WriteLine(i); 142 | } 143 | } 144 | } 145 | 146 | public void Load(string file) 147 | { 148 | ratings = new UserArticleRatingsTable(); 149 | 150 | using (FileStream fs = new FileStream(file, FileMode.Open)) 151 | using (GZipStream zip = new GZipStream(fs, CompressionMode.Decompress)) 152 | using (StreamReader r = new StreamReader(zip)) 153 | { 154 | numUsers = int.Parse(r.ReadLine()); 155 | numArticles = int.Parse(r.ReadLine()); 156 | numFeatures = int.Parse(r.ReadLine()); 157 | 158 | double averageGlobalRating = double.Parse(r.ReadLine()); 159 | 160 | double[] userBiases = new double[numUsers]; 161 | for (int userIndex = 0; userIndex < numUsers; userIndex++) 162 | { 163 | userBiases[userIndex] = double.Parse(r.ReadLine()); 164 | } 165 | 166 | double[] articleBiases = new double[numArticles]; 167 | for (int articleIndex = 0; articleIndex < numArticles; articleIndex++) 168 | { 169 | articleBiases[articleIndex] = double.Parse(r.ReadLine()); 170 | } 171 | 172 | double[][] userFeatures = new double[numUsers][]; 173 | for (int userIndex = 0; userIndex < numUsers; userIndex++) 174 | { 175 | userFeatures[userIndex] = new double[numFeatures]; 176 | 177 | for (int featureIndex = 0; featureIndex < numFeatures; featureIndex++) 178 | { 179 | userFeatures[userIndex][featureIndex] = double.Parse(r.ReadLine()); 180 | } 181 | } 182 | 183 | double[][] articleFeatures = new double[numArticles][]; 184 | for (int articleIndex = 0; articleIndex < numUsers; articleIndex++) 185 | { 186 | articleFeatures[articleIndex] = new double[numFeatures]; 187 | 188 | for (int featureIndex = 0; featureIndex < numFeatures; featureIndex++) 189 | { 190 | articleFeatures[articleIndex][featureIndex] = double.Parse(r.ReadLine()); 191 | } 192 | } 193 | 194 | svd = new SvdResult(averageGlobalRating, userBiases, articleBiases, userFeatures, articleFeatures); 195 | 196 | for (int i = 0; i < numUsers; i++) 197 | { 198 | int userId = int.Parse(r.ReadLine()); 199 | UserArticleRatings uat = new UserArticleRatings(userId, numArticles); 200 | 201 | for (int x = 0; x < numArticles; x++) 202 | { 203 | uat.ArticleRatings[x] = double.Parse(r.ReadLine()); 204 | } 205 | 206 | ratings.Users.Add(uat); 207 | } 208 | 209 | for (int i = 0; i < numUsers; i++) 210 | { 211 | ratings.UserIndexToID.Add(int.Parse(r.ReadLine())); 212 | } 213 | 214 | for (int i = 0; i < numArticles; i++) 215 | { 216 | ratings.ArticleIndexToID.Add(int.Parse(r.ReadLine())); 217 | } 218 | } 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /User Behavior/Recommenders/RandomRecommender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using UserBehavior.Objects; 7 | using UserBehavior.Parsers; 8 | 9 | namespace UserBehavior.Recommenders 10 | { 11 | public class RandomRecommender : IRecommender 12 | { 13 | private Random rand; 14 | 15 | public RandomRecommender() 16 | { 17 | rand = new Random(); 18 | } 19 | 20 | public void Train(UserBehaviorDatabase db) 21 | { 22 | } 23 | 24 | public double GetRating(int userId, int articleId) 25 | { 26 | return rand.NextDouble() * 5.0; 27 | } 28 | 29 | public List GetSuggestions(int userId, int numSuggestions) 30 | { 31 | List suggestions = new List(); 32 | 33 | for (int i = 0; i < numSuggestions; i++) 34 | { 35 | suggestions.Add(new Suggestion(userId, rand.Next(1, 3000), rand.NextDouble() * 5.0)); 36 | } 37 | 38 | return suggestions; 39 | } 40 | 41 | public void Load(string file) 42 | { 43 | throw new NotImplementedException(); 44 | } 45 | 46 | public void Save(string file) 47 | { 48 | throw new NotImplementedException(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /User Behavior/Recommenders/UserCollaborativeFilterRecommender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using UserBehavior.Abstractions; 9 | using UserBehavior.Comparers; 10 | using UserBehavior.Mathematics; 11 | using UserBehavior.Objects; 12 | using UserBehavior.Parsers; 13 | 14 | namespace UserBehavior.Recommenders 15 | { 16 | public class UserCollaborativeFilterRecommender : IRecommender 17 | { 18 | private IComparer comparer; 19 | private IRater rater; 20 | private UserArticleRatingsTable ratings; 21 | 22 | private int neighborCount; 23 | private int latentUserFeatureCount; 24 | 25 | public UserCollaborativeFilterRecommender(IComparer userComparer, IRater implicitRater, int numberOfNeighbors) 26 | : this(userComparer, implicitRater, numberOfNeighbors, 20) 27 | { 28 | } 29 | 30 | public UserCollaborativeFilterRecommender(IComparer userComparer, IRater implicitRater, int numberOfNeighbors, int latentFeatures) 31 | { 32 | comparer = userComparer; 33 | rater = implicitRater; 34 | neighborCount = numberOfNeighbors; 35 | latentUserFeatureCount = latentFeatures; 36 | } 37 | 38 | public void Train(UserBehaviorDatabase db) 39 | { 40 | UserBehaviorTransformer ubt = new UserBehaviorTransformer(db); 41 | ratings = ubt.GetUserArticleRatingsTable(rater); 42 | 43 | if (latentUserFeatureCount > 0) 44 | { 45 | SingularValueDecomposition svd = new SingularValueDecomposition(latentUserFeatureCount, 100); 46 | SvdResult results = svd.FactorizeMatrix(ratings); 47 | 48 | ratings.AppendUserFeatures(results.UserFeatures); 49 | } 50 | } 51 | 52 | public double GetRating(int userId, int articleId) 53 | { 54 | UserArticleRatings user = ratings.Users.FirstOrDefault(x => x.UserID == userId); 55 | List neighbors = GetNearestNeighbors(user, neighborCount); 56 | 57 | return GetRating(user, neighbors, articleId); 58 | } 59 | 60 | private double GetRating(UserArticleRatings user, List neighbors, int articleId) 61 | { 62 | int articleIndex = ratings.ArticleIndexToID.IndexOf(articleId); 63 | 64 | var nonZero = user.ArticleRatings.Where(x => x != 0); 65 | double avgUserRating = nonZero.Count() > 0 ? nonZero.Average() : 0.0; 66 | 67 | double score = 0.0; 68 | int count = 0; 69 | for (int u = 0; u < neighbors.Count; u++) 70 | { 71 | var nonZeroRatings = neighbors[u].ArticleRatings.Where(x => x != 0); 72 | double avgRating = nonZeroRatings.Count() > 0 ? nonZeroRatings.Average() : 0.0; 73 | 74 | if (neighbors[u].ArticleRatings[articleIndex] != 0) 75 | { 76 | score += neighbors[u].ArticleRatings[articleIndex] - avgRating; 77 | count++; 78 | } 79 | } 80 | if (count > 0) 81 | { 82 | score /= count; 83 | score += avgUserRating; 84 | } 85 | 86 | return score; 87 | } 88 | 89 | public List GetSuggestions(int userId, int numSuggestions) 90 | { 91 | int userIndex = ratings.UserIndexToID.IndexOf(userId); 92 | UserArticleRatings user = ratings.Users[userIndex]; 93 | List suggestions = new List(); 94 | 95 | var neighbors = GetNearestNeighbors(user, neighborCount); 96 | 97 | for (int articleIndex = 0; articleIndex < ratings.ArticleIndexToID.Count; articleIndex++) 98 | { 99 | // If the user in question hasn't rated the given article yet 100 | if (user.ArticleRatings[articleIndex] == 0) 101 | { 102 | double score = 0.0; 103 | int count = 0; 104 | for (int u = 0; u < neighbors.Count; u++) 105 | { 106 | if (neighbors[u].ArticleRatings[articleIndex] != 0) 107 | { 108 | // Calculate the weighted score for this article 109 | score += neighbors[u].ArticleRatings[articleIndex] - ((u + 1.0) / 100.0); 110 | count++; 111 | } 112 | } 113 | if (count > 0) 114 | { 115 | score /= count; 116 | } 117 | 118 | suggestions.Add(new Suggestion(userId, ratings.ArticleIndexToID[articleIndex], score)); 119 | } 120 | } 121 | 122 | suggestions.Sort((c, n) => n.Rating.CompareTo(c.Rating)); 123 | 124 | return suggestions.Take(numSuggestions).ToList(); 125 | } 126 | 127 | private List GetNearestNeighbors(UserArticleRatings user, int numUsers) 128 | { 129 | List neighbors = new List(); 130 | 131 | for (int i = 0; i < ratings.Users.Count; i++) 132 | { 133 | if (ratings.Users[i].UserID == user.UserID) 134 | { 135 | ratings.Users[i].Score = double.NegativeInfinity; 136 | } 137 | else 138 | { 139 | ratings.Users[i].Score = comparer.CompareVectors(ratings.Users[i].ArticleRatings, user.ArticleRatings); 140 | } 141 | } 142 | 143 | var similarUsers = ratings.Users.OrderByDescending(x => x.Score); 144 | 145 | return similarUsers.Take(numUsers).ToList(); 146 | } 147 | 148 | public void Save(string file) 149 | { 150 | using (FileStream fs = new FileStream(file, FileMode.Create)) 151 | using (GZipStream zip = new GZipStream(fs, CompressionMode.Compress)) 152 | using (StreamWriter w = new StreamWriter(zip)) 153 | { 154 | w.WriteLine(ratings.Users.Count); 155 | w.WriteLine(ratings.Users[0].ArticleRatings.Length); 156 | 157 | foreach (UserArticleRatings t in ratings.Users) 158 | { 159 | w.WriteLine(t.UserID); 160 | 161 | foreach (double v in t.ArticleRatings) 162 | { 163 | w.WriteLine(v); 164 | } 165 | } 166 | 167 | w.WriteLine(ratings.UserIndexToID.Count); 168 | 169 | foreach (int i in ratings.UserIndexToID) 170 | { 171 | w.WriteLine(i); 172 | } 173 | 174 | w.WriteLine(ratings.ArticleIndexToID.Count); 175 | 176 | foreach (int i in ratings.ArticleIndexToID) 177 | { 178 | w.WriteLine(i); 179 | } 180 | } 181 | } 182 | 183 | public void Load(string file) 184 | { 185 | ratings = new UserArticleRatingsTable(); 186 | 187 | using (FileStream fs = new FileStream(file, FileMode.Open)) 188 | using (GZipStream zip = new GZipStream(fs, CompressionMode.Decompress)) 189 | using (StreamReader r = new StreamReader(zip)) 190 | { 191 | long total = long.Parse(r.ReadLine()); 192 | int features = int.Parse(r.ReadLine()); 193 | 194 | for (long i = 0; i < total; i++) 195 | { 196 | int userId = int.Parse(r.ReadLine()); 197 | UserArticleRatings uat = new UserArticleRatings(userId, features); 198 | 199 | for (int x = 0; x < features; x++) 200 | { 201 | uat.ArticleRatings[x] = double.Parse(r.ReadLine()); 202 | } 203 | 204 | ratings.Users.Add(uat); 205 | } 206 | 207 | total = int.Parse(r.ReadLine()); 208 | 209 | for (int i = 0; i < total; i++) 210 | { 211 | ratings.UserIndexToID.Add(int.Parse(r.ReadLine())); 212 | } 213 | 214 | total = int.Parse(r.ReadLine()); 215 | 216 | for (int i = 0; i < total; i++) 217 | { 218 | ratings.ArticleIndexToID.Add(int.Parse(r.ReadLine())); 219 | } 220 | } 221 | } 222 | } 223 | } 224 | --------------------------------------------------------------------------------