Java学习-Day34

矩阵分解

一、推荐系统中的矩阵

对于一个推荐系统, 用户和物品之间的关系可以整理为如下这样的一个矩阵.

User-Item 1 2 3 4 5 ......
1 5 4 4.5 ? 3.9 ......
2 ? 4.5 ? 4.5 ? ......
3 4.5 ? 4.4 4 4 ......
4 ? 4.8 ? ? 4.5 ......
5 4 ? 4.5 5 ? ......
...... ...... ...... ...... ...... ...... ......

矩阵中每一行代表一个用户, 而每一列则代表一个物品. 若用户对物品有过评分, 则矩阵中处在用户对应的行与物品对应的列交叉的位置表示用户对物品的评分值. 矩阵中的问号代表用户对物品未评分. 这个矩阵就叫做User-Item 评分矩阵, 这个矩阵中的数在实际统计后大多数现显示为问号.

推荐系统需要做的事情就是对于任意一个用户, 预测出所有未评分物品的分值, 并按分值大小从高到低的顺序推荐将对应的物品推荐给用户.

二、SVD 算法

传统的推荐方法通过对 User-Item 评分矩阵采用基于近邻的协同过滤来发现与用户具有相似偏好的其他用户, 进而产生推荐, 也就是之前所写过的 MBR. 但这种方法存在着数据稀疏和信息冗余大的缺陷, 当矩阵中评分元素稀疏, 以及信息量并非随着向量维度增加而线性增加.

那么就有人利用线性代数中的奇异值分解, 把原来大的评分矩阵分解称小矩阵的乘积.

对于奇异值分解的描述: 假设矩阵 \(M\) 是一个 \(m \times n\) 的矩阵, 则一定存在一个分解 \(M = U \Sigma V^{\mathrm{T}}\), 其中 \(U\)\(m \times m\) 的正交矩阵, \(V\)\(n \times n\) 的正交矩阵, \(\Sigma\)\(m \times m\) 的对角阵.

\[ M_{m \times n} = U_{m \times m} \ \Sigma_{m \times m} \ V_{n \times n} \]

但是传统奇异值分解不允许矩阵中有空白元素存在, 也就是说评分矩阵中不能存在问号. 这对推荐系统来说是不可能做到的. 基于这个问题, 所以 Simon Funk 就提出了 Funk-SVD 算法.

三、Funk-SVD 算法

例如评分矩阵 \(M\) 是一个 \(m \times n\) 的矩阵, 一共有 \(m\) 个用户, \(n\) 个物品. 通过一系列运算将矩阵 \(M\) 转化为两个矩阵 \(P\)\(Q\), 矩阵 \(U\) 的大小是 \(m \times k\), 矩阵 \(Q\) 的大小是 \(k \times n\). 式子如下所示:

\[ M_{m \times n} \approx P_{m \times k} Q_{k \times n} \]

因为矩阵 \(M\) 中存在未知, 我们只是在对这个矩阵进行拟合, 所以是约等于.

该方法基于这样一个假设: 用户对一个物品的喜爱程度主要由 \(k\) 个因素决定, \(P_{ni}\) 表示第 \(n\) 个用户对第 \(i\) 个因素的偏好程度, 而 \(Q_{ix}\) 表示第 \(x\) 个物品满足第 \(i\) 个因素的程度, \(M_{nx}\) 表示用户 \(n\) 对物品 \(x\) 最终的喜好程度.

那么对于原始矩阵中有评分的位置 \(M_{UI}\) 来说, 其在分解后矩阵中对应的值为: \[ M'_{UI} = {\textstyle \sum_{k=1}^{K}}P_{U,k} \ Q_{k,I} \]

其中 \(U\) 表示用户, \(I\) 表示物品.

那么对于整个评分矩阵而言, 总的损失就是: \[ SSE = E^2 = {\textstyle \sum_{U,I}}(M_{U,I} \ - \ M'_{U,I})^2 \]

现在的问题就转化为了求在损失 \(SSE\) 最小时的矩阵 \(P\)\(Q\).

四、随机梯度下降

梯度是一个向量, 表示的是一个函数在该点处沿梯度的方向变化最快, 变化率最大, 而梯度下降的方向就是指的负梯度方向. 想象下山的过程, 越是陡峭那么下山的速度就会越快 (这个过程不保证生命安全). 我们要找的就是类似最快下山这一过程.

梯度下降需要考虑所有的样本, 在求函数的最小值最后会在驻点或者极值点停下来, 所以这样的方法只适合凸函数. 例如函数 \(f(x)=x^2\) 就是一个凸函数, 满足 \(f(\frac{x_1+x_2}{2}) \le \frac{f(x_1)+f(x_2)}{2}\). 其图像如下所示:

在这个函数上做梯度下降总会到达 \((0,0)\) 这个点, 当然这也是这个函数的最小值的点.

随机梯度下降法主要是用来解决求和形式的优化问题, 与上面需要优化的目标函数一致. 其思想也很简单, 既然对于求和式中每一项求梯度很麻烦, 那么干脆就随机选其中一项计算梯度当作总的梯度来使用. 也就是说梯度下降是整体样本, 随机梯度下降是单个样本.

上面的目标函数为: \[ SSE = E^2 = {\textstyle \sum_{U,I}}(M_{U,I} \ - \ {\textstyle \sum_{k=1}^{K}}P_{U,k} \ Q_{k,I} )^2 \]

\(SSE\) 是关于 \(P\)\(Q\) 的多元函数, 当随机选定 \(U\)\(I\) 后, 需要枚举所有的 \(k\), 并且对 \(P_{U,k}\) 以及 \(Q_{k,I}\) 求偏导数. \[ \frac{\partial}{\partial P_{u,k}}{E_{U,I}}^2 = 2 E_{U,I}\frac{\partial E_{U,I}}{\partial P_{u,k}} = -2E_{U,I}Q_{k,I} \]

\[ \frac{\partial}{\partial Q_{k,I}}{E_{U,I}}^2 = 2 E_{U,I}\frac{\partial E_{U,I}}{\partial Q_{k,I}} = -2E_{U,I}P_{U,k} \]

五、具体实现

1. 描述

生成两个随机矩阵, 这两个随机矩阵就是之前式子中提到的 \(P\)\(Q\). 利用随机梯度下降的公式求出变化的趋势, 再乘以一个非常小的变化量. 使用这样的方式对这两个矩阵不断更新.

代码中并没有达到完全收敛, 而是设定了一个矩阵更新次数的阈值. 并在每 500 轮次时输出平均绝对误差和均方根误差, 用于观察收敛的效果.

2. 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
package recommendersystem;

import java.io.*;
import java.util.Random;

/**
* Matrix factorization for recommender systems.
*
* @author Shi-Huai Wen Email: shihuaiwen@outlook.com.
*/
public class MatrixFactorization {
/**
* Used to generate random numbers.
*/
Random rand = new Random();

/**
* Number of users.
*/
int numUsers;

/**
* Number of items.
*/
int numItems;

/**
* Number of ratings.
*/
int numRatings;

/**
* Training data.
*/
Triple[] dataset;

/**
* A parameter for controlling learning regular.
*/
double alpha;

/**
* A parameter for controlling the learning speed.
*/
double lambda;

/**
* The low rank of the small matrices.
*/
int rank;

/**
* The user matrix U.
*/
double[][] userSubspace;

/**
* The item matrix V.
*/
double[][] itemSubspace;

/**
* The lower bound of the rating value.
*/
double ratingLowerBound;

/**
* The upper bound of the rating value.
*/
double ratingUpperBound;

/**
* ***********************
* The first constructor.
*
* @param paraFilename The data filename.
* @param paraNumUsers The number of users.
* @param paraNumItems The number of items.
* @param paraNumRatings The number of ratings.
* ***********************
*/
public MatrixFactorization(String paraFilename, int paraNumUsers, int paraNumItems,
int paraNumRatings, double paraRatingLowerBound, double paraRatingUpperBound) {
numUsers = paraNumUsers;
numItems = paraNumItems;
numRatings = paraNumRatings;
ratingLowerBound = paraRatingLowerBound;
ratingUpperBound = paraRatingUpperBound;

try {
readData(paraFilename, paraNumUsers, paraNumItems, paraNumRatings);
} catch (Exception ee) {
System.out.println("File " + paraFilename + " cannot be read! " + ee);
System.exit(0);
} // Of try
}// Of the first constructor

/**
* ***********************
* Set parameters.
*
* @param paraRank The given rank.
* ***********************
*/
public void setParameters(int paraRank, double paraAlpha, double paraLambda) {
rank = paraRank;
alpha = paraAlpha;
lambda = paraLambda;
}// Of setParameters

/**
* ***********************
* Read the data from the file.
*
* @param paraFilename The given file.
* @throws IOException
* ***********************
*/
public void readData(String paraFilename, int paraNumUsers, int paraNumItems,
int paraNumRatings) throws IOException {
File tempFile = new File(paraFilename);
if (!tempFile.exists()) {
System.out.println("File " + paraFilename + " does not exists.");
System.exit(0);
} // Of if
BufferedReader tempBufferReader = new BufferedReader(new FileReader(tempFile));

// Allocate space.
dataset = new Triple[paraNumRatings];
String tempString;
String[] tempStringArray;
for (int i = 0; i < paraNumRatings; i++) {
tempString = tempBufferReader.readLine();
tempStringArray = tempString.split(",");
dataset[i] = new Triple(Integer.parseInt(tempStringArray[0]),
Integer.parseInt(tempStringArray[1]), Double.parseDouble(tempStringArray[2]));
} // Of for i

tempBufferReader.close();
}// Of readData

/**
* ***********************
* Initialize subspaces. Each value is in [0, 1].
* ***********************
*/
void initializeSubspaces() {
userSubspace = new double[numUsers][rank];

for (int i = 0; i < numUsers; i++) {
for (int j = 0; j < rank; j++) {
userSubspace[i][j] = rand.nextDouble();
} // Of for j
} // Of for i

itemSubspace = new double[numItems][rank];
for (int i = 0; i < numItems; i++) {
for (int j = 0; j < rank; j++) {
itemSubspace[i][j] = rand.nextDouble();
} // Of for j
} // Of for i
}// Of initializeSubspaces

/**
* ***********************
* Predict the rating of the user to the item
*
* @param paraUser The user index.
* ***********************
*/
public double predict(int paraUser, int paraItem) {
double resultValue = 0;
for (int i = 0; i < rank; i++) {
// The row vector of a user and the column vector of an item
resultValue += userSubspace[paraUser][i] * itemSubspace[paraItem][i];
} // Of for i
return resultValue;
}// Of predict

/**
* ***********************
* Train.
*
* @param paraRounds The number of rounds.
* ***********************
*/
public void train(int paraRounds) {
initializeSubspaces();

for (int i = 0; i < paraRounds; i++) {
updateNoRegular();
if (i % 500 == 0) {
// Show the process
System.out.println("Round " + i);
System.out.println("MAE: " + mae());
System.out.println("RSME: " + rsme());
} // Of if
} // Of for i
}// Of train

/**
* ***********************
* Update sub-spaces using the training data.
* ***********************
*/
public void updateNoRegular() {
for (int i = 0; i < numRatings; i++) {
int tempUserId = dataset[i].user;
int tempItemId = dataset[i].item;
double tempRate = dataset[i].rating;

// Residual
double tempResidual = tempRate - predict(tempUserId, tempItemId);

// Update user subspace
double tempValue;
for (int j = 0; j < rank; j++) {
tempValue = 2 * tempResidual * itemSubspace[tempItemId][j];
userSubspace[tempUserId][j] += alpha * tempValue;
} // Of for j

// Update item subspace
for (int j = 0; j < rank; j++) {
tempValue = 2 * tempResidual * userSubspace[tempUserId][j];

itemSubspace[tempItemId][j] += alpha * tempValue;
} // Of for j
} // Of for i
}// Of updateNoRegular

/**
* ***********************
* Compute the RSME.
*
* @return RSME of the current factorization.
* ***********************
*/
public double rsme() {
double resultRsme = 0;
int tempTestCount = 0;

for (int i = 0; i < numRatings; i++) {
int tempUserIndex = dataset[i].user;
int tempItemIndex = dataset[i].item;
double tempRate = dataset[i].rating;

double tempPrediction = predict(tempUserIndex, tempItemIndex);
// DataInfo.mean_rating;

if (tempPrediction < ratingLowerBound) {
tempPrediction = ratingLowerBound;
} else if (tempPrediction > ratingUpperBound) {
tempPrediction = ratingUpperBound;
} // Of if

double tempError = tempRate - tempPrediction;
resultRsme += tempError * tempError;
tempTestCount++;
} // Of for i

return Math.sqrt(resultRsme / tempTestCount);
}// Of rsme

/**
* ***********************
* Compute the MAE.
*
* @return MAE of the current factorization.
* ***********************
*/
public double mae() {
double resultMae = 0;
int tempTestCount = 0;

for (int i = 0; i < numRatings; i++) {
int tempUserIndex = dataset[i].user;
int tempItemIndex = dataset[i].item;
double tempRate = dataset[i].rating;

double tempPrediction = predict(tempUserIndex, tempItemIndex);

if (tempPrediction < ratingLowerBound) {
tempPrediction = ratingLowerBound;
} // Of if
if (tempPrediction > ratingUpperBound) {
tempPrediction = ratingUpperBound;
} // Of if

double tempError = tempRate - tempPrediction;

resultMae += Math.abs(tempError);
tempTestCount++;
} // Of for i

return (resultMae / tempTestCount);
}// Of mae

/**
* ***********************
* Test accuracy
*
* Out MAE and RSME
* ***********************
*/
public static void testTrainingTesting(String paraFilename, int paraNumUsers, int paraNumItems,
int paraNumRatings, double paraRatingLowerBound, double paraRatingUpperBound,
int paraRounds) {
try {
// Step 1. read the training and testing data
MatrixFactorization tempMF = new MatrixFactorization(paraFilename, paraNumUsers,
paraNumItems, paraNumRatings, paraRatingLowerBound, paraRatingUpperBound);

// Step 2. read the training and testing data
tempMF.setParameters(5, 0.0001, 0.005);

// Step 3. update and predict
System.out.println("Begin Training ! ! !");
tempMF.train(paraRounds);

double tempMAE = tempMF.mae();
double tempRSME = tempMF.rsme();
System.out.println("Finally, MAE = " + tempMAE + ", RSME = " + tempRSME);
} catch (Exception e) {
e.printStackTrace();
} // Of try
}// Of testTrainingTesting

/**
* ************************
* Test this class.
*
* @param args Not used now.
* ************************
*/
public static void main(String[] args) {
testTrainingTesting("D:/Work/sampledata/movielens-943u1682m.txt", 943, 1682, 10000, 1, 5, 2000);
}// Of main

public static class Triple {
public int user;
public int item;
public double rating;

/**
* ********************
* The constructor.
* ********************
*/
public Triple(int paraUser, int paraItem, double paraRating) {
user = paraUser;
item = paraItem;
rating = paraRating;
}// Of the first constructor

/**
* ********************
* Show me.
* ********************
*/
public String toString() {
return "" + user + ", " + item + ", " + rating;
}// Of toString
}// Of class Triple
} // Of class MatrixFactorization

3. 运行截图

总结

随机梯度下降的方法有点像牛顿迭代法求平方根的感觉. 然后最神奇的地方还是考虑到隐藏属性这个点, 然后依靠随机后再收敛的方法处理, 就不需要考虑用作计算的中间矩阵的各个值. 纵使这些值能够得到, 但也免不了中间矩阵有空白出现.