推荐系统系列(一):FM理论与实践
背景
在推荐领域CTR(click-through rate)预估任务中,最常用到的baseline模型就是LR(Logistic Regression)。对数据进行特征工程,构造出大量单特征,编码之后送入模型。这种线性模型的优势在于,运算速度快可解释性强,在特征挖掘完备且训练数据充分的前提下能够达到一定精度。但这种模型的缺点也是较为明显的:
- 模型并未考虑到特征之间的关系 \(y=w_0+\sum_{i=1}^{n}w_ix_i\) 。在实践经验中,对特征进行交叉组合往往能够更好地提升模型效果。
- 对于多取值的categorical特征进行one-hot编码,具有高度稀疏性,带来维度灾难问题。
FM(Factorization Machine)模型就是针对在特征组合过程中遇到的上述问题而提出的一种高效的解决方案[1]。由于FM优越的性能表现,后续出现了一系列FM变种模型,从浅层模型到深度推荐模型中都有FM的影子。
分析
1. FM定义
FM以特征组合进行切入点,在公式定义中引入特征交叉项,弥补了一般线性模型未考虑特征间关系的缺憾。公式如下(FM模型可拓展到高阶,但为简化且不失一般性,这里只讨论二阶交叉)[1]:
\]
与一般线性模型相比,公式(1)仅多了一个二阶交叉项,模型参数多了 \(\frac{n(n+1)}{2}\) 个。虽然这种显式交叉的方式能够刻画特征间关系,但是对公式求解带来困难。
因为大量特征进行one-hot表示之后具有高度稀疏性的问题,所以公式(1)中的 \(x_ix_j\) 同样会产生大量的0值。参数学习不充分,直接导致\(w_{ij}\) 无法通过训练得到。(解释:令\(x_ix_j=X\),则\(\frac{\partial{y}}{\partial{w_{ij}}}=X\) ,又因 \(X=0\),所以\(w_{ij}^{new}=w_{ij}^{old}+{\alpha}X=w_{ij}^{old}\) ,梯度为0参数无法更新。)
导致这种情况出现的根源在于:特征过于稀疏。我们期望的是找到一种方法,使得 \(w_{ij}\) 的求解不受特征稀疏性的影响。
2. 公式改写
为了克服上述困难,需要对FM公式进行改写,使得求解更加顺利。受 矩阵分解 的启发,对于每一个特征 \(x_i\) 引入辅助向量(隐向量)\(V_i=(v_{i1},v_{i2},\cdots,v_{ik})\),然后利用\(V_iV_j^T\) 对\(w_{ij}\) 进行求解。即,做如下假设: \(w_{ij} \approx V_iV_j^T\) 。
引入隐向量的好处是:
二阶项的参数量由原来的 \(\frac{n(n-1)}{2}\) 降为 \(kn\) 。
原先参数之间并无关联关系,但是现在通过隐向量可以建立关系。如,之前 \(w_{ij}\) 与 \(w_{ik}\) 无关,但是现在 $w_{ij}=\langle V_i,V_j\rangle ,w_{ik}=\langle V_i,V_k\rangle $ 两者有共同的 \(V_i\) ,也就是说,所有包含 \(x_ix_j\) 的非零组合特征(存在某个 \(j\neq i\) ,使得 \(x_ix_j\neq 0\) )的样本都可以用来学习隐向量 \(V_i\) ,这很大程度上避免了数据稀疏性造成的影响。[2]
现在可以将公式(1)进行改写:
\]
重心转移到如何求解公式(2)后面的二阶项。
预备知识:
首先了解 对称 矩阵上三角求和,设矩阵为 \(M\):
\begin{matrix}
m_{11} & m_{12} & \cdots & m_{1n} \\
m_{21} & m_{22} & \cdots & m_{1n} \\
\vdots & \vdots & \ddots & \vdots \\
m_{n1} & m_{n2} & \cdots & m_{nn} \\
\end{matrix}
\right)_{n*n}
\]
其中,\(m_{ij}=m_{ji}\) 。
令上三角元素和为 \(A\) ,即 \(\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}m_{ij}=A\) 。那么,\(M\) 的所有元素之和等于 \(2*A+tr(M)\) ,\(tr(M)\)为矩阵的迹。
\]
可得,
\]
正式改写:
有了上述预备知识,可以对公式(2)的二阶项进行推导:
& \sum_{i=1}^{n-1}\sum_{j=i+1}^n\langle V_i,V_j\rangle x_ix_j \notag \\
={} & \frac{1}{2}*\left\{\sum_{i=1}^{n}\sum_{j=1}^{n}\langle V_i,V_j\rangle x_ix_j-\sum_{i=1}^{n}\langle V_i,V_i\rangle x_ix_i\right\} \notag \\
={} & \frac{1}{2}*\left\{\sum_{i=1}^{n}\sum_{j=1}^{n}\sum_{f=1}^{k}v_{if}v_{jf}x_ix_j-\sum_{i=1}^{n}\sum_{f=1}^{k}v_{if}v_{if}x_ix_i\right\} \notag \\
={} & \frac{1}{2}*\sum_{f=1}^{k}\left\{\sum_{i=1}^{n}\sum_{j=1}^{n}v_{if}x_iv_{jf}x_j-\sum_{i=1}^{n}v_{if}^{2}x_{i}^2\right\} \notag \\
={} & \frac{1}{2}*\sum_{f=1}^{k}\left\{\left(\sum_{i=1}^{n}v_{if}x_i\right)\left(\sum_{j=1}^{n}v_{jf}x_j\right)-\sum_{i=1}^{n}v_{if}^{2}x_{i}^2\right\} \notag \\
={} & \frac{1}{2}*\sum_{f=1}^{k}\left\{\left(\sum_{i=1}^{n}v_{if}x_i\right)^{2}-\sum_{i=1}^{n}v_{if}^{2}x_{i}^2\right\} \notag \\
\end{align}\tag{3}
\]
结合(2)(3),可以得到:
y ={} & w_0+\sum_{i=1}^nw_ix_i+\sum_{i=1}^{n-1}\sum_{j=i+1}^n\langle V_i,V_j\rangle x_ix_j \notag \\
={} & w_0+\sum_{i=1}^nw_ix_i+\frac{1}{2}*\sum_{f=1}^{k}\left\{\left(\sum_{i=1}^{n}v_{if}x_i\right)^{2}-\sum_{i=1}^{n}v_{if}^{2}x_{i}^2\right\} \notag \\
\end{align} \tag{4}
\]
至此,我们得到了想要的模型表达式。
为什么要将公式(2)改写为公式(4),是因为在改写之前,计算 \(y\) 的复杂度为 \(O(kn^2)\) ,改写后的计算复杂度为 \(O(kn)\) ,提高模型推断速度。
3. FM求解
到目前为止已经得到了FM的模型表示(4),如何对模型参数求解呢?可以使用常见的梯度下降法对参数进行求解,为了对参数进行梯度下降更新,需要计算模型各参数的梯度表达式:
当参数为 \(w_0\) 时,\(\frac{\partial{y}}{\partial{w_0}}=1\) 。
当参数为 \(w_i\) 时,\(\frac{\partial{y}}{\partial{w_i}}=x_i\) 。
当参数为 \(v_{if}\) 时,只需要关注模型高阶项,当计算参数 \(v_{if}\) 的梯度时,其余无关参数可看做常数。
\frac{\partial{y}}{\partial{v_{if}}}
={} & \partial{\frac{1}{2}\left\{\left(\sum_{i=1}^{n}v_{if}x_i\right)^{2}-\sum_{i=1}^{n}v_{if}^{2}x_{i}^2\right\}}/\partial{v_{if}} \notag \\
={} & \frac{1}{2}* \left\{
\frac{
\partial{
\left\{ \sum_{i=1}^{n}v_{if}x_i
\right\}^2
}
}{\partial{v_{if}}}
- \frac{
\partial{
\left\{
\sum_{i=1}^{n}v_{if}^{2}x_{i}^2
\right\}
}
}{\partial{v_{if}}}
\right\} \notag \\
\end{align} \tag{5}
\]
其中:
\partial{
\left\{
\sum_{i=1}^{n}v_{if}^{2}x_{i}^2
\right\}
}
}{\partial{v_{if}}}
=
2x_{i}^2v_{if}
\tag{6}
\]
令 \(\lambda=\sum_{i=1}^{n}v_{if}x_i\) ,则:
\frac{
\partial{
\left\{ \sum_{i=1}^{n}v_{if}x_i
\right\}^2
}
}{\partial{v_{if}}}
={} & \frac{\partial{\lambda^2}}{\partial{v_{if}}} \notag \\
={} & \frac{\partial{\lambda^2}}{\partial{\lambda}}
\frac{\partial{\lambda}}{\partial{v_{if}}} \notag \\
={} & 2\lambda*\frac{\partial{\sum_{i=1}^{n}v_{if}x_i}}{\partial{v_{if}}} \notag \\
={} & 2\lambda*x_i \notag \\
={} & 2*x_i*\sum_{j=1}^{n}v_{jf}x_j \notag \\
\end{align} \tag{7}
\]
结合公式(5~7),可得:
x_i\sum_{j=1}^{n}v_{jf}x_j-x_{i}^2v_{if}
\tag{8}
\]
综上,最终模型各参数的梯度表达式如下:
\frac{\partial{y}}{\partial{\theta}} =
\begin{cases}
1, & \text{if } \theta \text{ is } w_0; \\
x_i, & \text{if } \theta \text{ is } w_i; \\
x_i\sum_{j=1}^{n}v_{jf}x_j-x_{i}^2v_{if}, & \text{if } \theta \text{ is } v_{if}.
\end{cases}
\end{equation} \notag
\]
4. 性能分析
由第2小节可知,FM进行推断的时间复杂度为 \(O(kn)\) 。分析训练的复杂度,依据参数的梯度表达式,\(\sum_{j=1}^{n}v_{jf}x_{j}\) 与 \(i\) 无关,在参数更新时可以首先将所有的 \(\sum_{j=1}^{n}v_{jf}x_{j}\) 计算出来,复杂度为 \(O(kn)\) ,后续更新所有参数的时间复杂度均为 \(O(1)\) ,参数量为 \(1+n+kn\) ,所以最终训练的时间复杂度同样为 \(O(kn)\) ,其中 \(n\) 为特征数,\(k\) 为隐向量维数。
FM训练与预测的时间复杂度均为 \(O(kn)\) ,是一种十分高效的模型。
5. 优缺点
优点 [1]:
In total, the advantages of our proposed FM are:
FMs allow parameter estimation under very sparse data where SVMs fail.
FMs have linear complexity, can be optimized in the primal and do not rely on support vectors like SVMs. We show that FMs scale to large datasets like Netflix with 100 millions of training instances.
FMs are a general predictor that can work with any real valued feature vector. In contrast to this, other state-of- the-art factorization models work only on very restricted input data. We will show that just by defining the feature vectors of the input data, FMs can mimic state-of-the-art models like biased MF, SVD++, PITF or FPMC.
缺点:
- 每个特征只引入了一个隐向量,不同类型特征之间交叉没有区分性。FFM模型正是以这一点作为切入进行改进。
实验
FM既可以应用在回归任务,也可以应用在分类任务中。如,在二分类任务中只需在公式(2)最外层套上 \(sigmoid\) 函数即可,上述解析都是基于回归任务来进行推导的。
关于模型最终的损失函数同样可以有多种形式,如回归任务可以使用 \(MSE\) ,分类任务可以使用 \(Cross Entropy\) 等。
1. 代码演示
虽然知道可以通过引入辅助向量进行计算,但是辅助向量是如何与特征 \(x_i\) 建立联系的,换句话说,如何通过 \(x_i\) 得到辅助向量 \(V_i\) ?在使用神经网络实现FM的过程中,将 \(x_i\) 的 \(embedding\) 作为辅助向量,最终得到的 \(embedding\) 向量组也可以看作是对应特征的低维稠密表征,可以应用到其他下游任务中。
1.1 回归任务
本文使用了 \(MovieLens 100K Dataset\) [3] 作为实验输入,特征组分别为用户编号、电影编号,用户对电影的历史评分作为 \(Label\) 。
具体代码实现如下:
# -*- coding:utf-8 -*-
import pandas as pd
import numpy as np
from scipy.sparse import csr
from itertools import count
from collections import defaultdict
import tensorflow as tf
def vectorize_dic(dic, label2index=None, hold_num=None):
if label2index == None:
d = count(0)
label2index = defaultdict(lambda: next(d)) # 数值映射表
sample_num = len(list(dic.values())[0]) # 样本数
feat_num = len(list(dic.keys())) # 特征数
total_value_num = sample_num * feat_num
col_ix = np.empty(total_value_num, dtype=int)
i = 0
for k, lis in dic.items():
col_ix[i::feat_num] = [label2index[str(k) + str(el)] for el in lis]
i += 1
row_ix = np.repeat(np.arange(sample_num), feat_num)
data = np.ones(total_value_num)
if hold_num is None:
hold_num = len(label2index)
left_data_index = np.where(col_ix < hold_num) # 为了剔除不在train set中出现的test set数据
return csr.csr_matrix(
(data[left_data_index], (row_ix[left_data_index], col_ix[left_data_index])),
shape=(sample_num, hold_num)), label2index
def batcher(X_, y_, batch_size=-1):
assert X_.shape[0] == len(y_)
n_samples = X_.shape[0]
if batch_size == -1:
batch_size = n_samples
if batch_size < 1:
raise ValueError('Parameter batch_size={} is unsupported'.format(batch_size))
for i in range(0, n_samples, batch_size):
upper_bound = min(i + batch_size, n_samples)
ret_x = X_[i:upper_bound]
ret_y = y_[i:upper_bound]
yield(ret_x, ret_y)
def load_dataset():
cols = ['user', 'item', 'rating', 'timestamp']
train = pd.read_csv('data/ua.base', delimiter='\t', names=cols)
test = pd.read_csv('data/ua.test', delimiter='\t', names=cols)
x_train, label2index = vectorize_dic({'users': train.user.values, 'items': train.item.values})
x_test, label2index = vectorize_dic({'users': test.user.values, 'items': test.item.values}, label2index, x_train.shape[1])
y_train = train.rating.values
y_test = test.rating.values
x_train = x_train.todense()
x_test = x_test.todense()
return x_train, x_test, y_train, y_test
x_train, x_test, y_train, y_test = load_dataset()
print("x_train shape: ", x_train.shape)
print("x_test shape: ", x_test.shape)
print("y_train shape: ", y_train.shape)
print("y_test shape: ", y_test.shape)
vec_dim = 10
batch_size = 1000
epochs = 10
learning_rate = 0.001
sample_num, feat_num = x_train.shape
x = tf.placeholder(tf.float32, shape=[None, feat_num], name="input_x")
y = tf.placeholder(tf.float32, shape=[None,1], name="ground_truth")
w0 = tf.get_variable(name="bias", shape=(1), dtype=tf.float32)
W = tf.get_variable(name="linear_w", shape=(feat_num), dtype=tf.float32)
V = tf.get_variable(name="interaction_w", shape=(feat_num, vec_dim), dtype=tf.float32)
linear_part = w0 + tf.reduce_sum(tf.multiply(x, W), axis=1, keep_dims=True)
interaction_part = 0.5 * tf.reduce_sum(tf.square(tf.matmul(x, V)) - tf.matmul(tf.square(x), tf.square(V)), axis=1, keep_dims=True)
y_hat = linear_part + interaction_part
loss = tf.reduce_mean(tf.square(y - y_hat))
train_op = tf.train.AdamOptimizer(learning_rate).minimize(loss)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for e in range(epochs):
step = 0
print("epoch:{}".format(e))
for batch_x, batch_y in batcher(x_train, y_train, batch_size):
sess.run(train_op, feed_dict={x:batch_x, y:batch_y.reshape(-1, 1)})
step += 1
if step % 10 == 0:
for val_x, val_y in batcher(x_test, y_test):
train_loss = sess.run(loss, feed_dict={x:batch_x, y:batch_y.reshape(-1, 1)})
val_loss = sess.run(loss, feed_dict={x:val_x, y:val_y.reshape(-1, 1)})
print("batch train_mse={}, val_mse={}".format(train_loss, val_loss))
for val_x, val_y in batcher(x_test, y_test):
val_loss = sess.run(loss, feed_dict={x: val_x, y: val_y.reshape(-1, 1)})
print("test set rmse = {}".format(np.sqrt(val_loss)))
实验结果:
epoch:0
batch train_mse=19.54930305480957, val_mse=19.687997817993164
batch train_mse=16.957233428955078, val_mse=19.531404495239258
batch train_mse=18.544944763183594, val_mse=19.376962661743164
batch train_mse=18.870519638061523, val_mse=19.222412109375
batch train_mse=18.769777297973633, val_mse=19.070764541625977
batch train_mse=19.383392333984375, val_mse=18.915040969848633
batch train_mse=17.26403045654297, val_mse=18.75937843322754
batch train_mse=17.652183532714844, val_mse=18.6033935546875
batch train_mse=18.331804275512695, val_mse=18.447608947753906
......
epoch:9
batch train_mse=1.394300103187561, val_mse=1.4516444206237793
batch train_mse=1.2031371593475342, val_mse=1.4285767078399658
batch train_mse=1.1761484146118164, val_mse=1.4077649116516113
batch train_mse=1.134848952293396, val_mse=1.3872103691101074
batch train_mse=1.2191411256790161, val_mse=1.3692644834518433
batch train_mse=1.572729468345642, val_mse=1.3509554862976074
batch train_mse=1.3323310613632202, val_mse=1.3339732885360718
batch train_mse=1.1601723432540894, val_mse=1.3183823823928833
batch train_mse=1.2751621007919312, val_mse=1.3023829460144043
test set rmse = 1.1405380964279175
1.2 分类任务
使用更全的 \(MovieLens 100K Dataset\) 特征,将评分大于3分的样本作为正类,其他为负类,构造二分类任务。核心代码如下:
class FM(object):
def __init__(self, vec_dim, feat_num, lr, lamda):
self.vec_dim = vec_dim
self.feat_num = feat_num
self.lr = lr
self.lamda = lamda
self._build_graph()
def _build_graph(self):
self.add_input()
self.inference()
def add_input(self):
self.x = tf.placeholder(tf.float32, shape=[None, self.feat_num], name='input_x')
self.y = tf.placeholder(tf.float32, shape=[None], name='input_y')
def inference(self):
with tf.variable_scope('linear_part'):
w0 = tf.get_variable(name='bias', shape=[1], dtype=tf.float32)
self.W = tf.get_variable(name='linear_w', shape=[self.feat_num], dtype=tf.float32)
self.linear_part = w0 + tf.reduce_sum(tf.multiply(self.x, self.W), axis=1)
with tf.variable_scope('interaction_part'):
self.V = tf.get_variable(name='interaction_w', shape=[self.feat_num, self.vec_dim], dtype=tf.float32)
self.interaction_part = 0.5 * tf.reduce_sum(
tf.square(tf.matmul(self.x, self.V)) - tf.matmul(tf.square(self.x), tf.square(self.V)),
axis=1
)
self.y_logits = self.linear_part + self.interaction_part
self.y_hat = tf.nn.sigmoid(self.y_logits)
self.pred_label = tf.cast(self.y_hat > 0.5, tf.int32)
self.loss = -tf.reduce_mean(self.y*tf.log(self.y_hat+1e-8) + (1-self.y)*tf.log(1-self.y_hat+1e-8))
self.reg_loss = self.lamda*(tf.reduce_mean(tf.nn.l2_loss(self.W)) + tf.reduce_mean(tf.nn.l2_loss(self.V)))
self.total_loss = self.loss + self.reg_loss
self.train_op = tf.train.AdamOptimizer(self.lr).minimize(self.total_loss)
实验结果:
Iter: 59400, Train acc: 0.7812, Val acc: 0.6867, Val auc: 0.7285, Val loss: 0.614005, Flag:
Iter: 59600, Train acc: 0.8125, Val acc: 0.684, Val auc: 0.7294, Val loss: 0.615628, Flag: *
Iter: 59800, Train acc: 0.875, Val acc: 0.6665, Val auc: 0.7282, Val loss: 0.625017, Flag:
Iter: 60000, Train acc: 0.9375, Val acc: 0.6767, Val auc: 0.7282, Val loss: 0.617686, Flag:
Iter: 60200, Train acc: 0.75, Val acc: 0.6815, Val auc: 0.7277, Val loss: 0.614763, Flag:
Iter: 60400, Train acc: 0.9062, Val acc: 0.681, Val auc: 0.7283, Val loss: 0.614414, Flag:
Iter: 60600, Train acc: 0.6875, Val acc: 0.6853, Val auc: 0.7291, Val loss: 0.621548, Flag:
Iter: 60800, Train acc: 0.625, Val acc: 0.679, Val auc: 0.7288, Val loss: 0.617327, Flag:
Iter: 61000, Train acc: 0.7812, Val acc: 0.6835, Val auc: 0.7293, Val loss: 0.616952, Flag:
Iter: 61200, Train acc: 0.8125, Val acc: 0.686, Val auc: 0.7292, Val loss: 0.614379, Flag:
Iter: 61400, Train acc: 0.6562, Val acc: 0.688, Val auc: 0.7284, Val loss: 0.613859, Flag:
Iter: 61600, Train acc: 0.6875, Val acc: 0.6725, Val auc: 0.7279, Val loss: 0.618824, Flag:
No optimization for a long time, auto-stopping...
====== let's test =====
Test acc: 0.6833, Test auc: 0.7369
2. 注意事项
- 虽然FM可以应用于任意数值类型的数据上,但是需要注意对输入特征数值进行预处理。优先进行特征归一化,其次再进行样本归一化。[2]
- FM不仅可以用于rank阶段,同时可以用于向量召回:好文推荐
reference
- [1] Rendle, S. (2010, December). Factorization machines. In 2010 IEEE International Conference on Data Mining (pp. 995-1000). IEEE.
- [2] https://tech.meituan.com/2016/03/03/deep-understanding-of-ffm-principles-and-practices.html
- [3] https://grouplens.org/datasets/movielens/100k/
- [4] https://www.jianshu.com/p/152ae633fb00
- [5] http://nowave.it/factorization-machines-with-tensorflow.html
- [6] https://blog.csdn.net/xxiaobaib/article/details/92801244
- [7] [https://github.com/wyl6/Recommender-Systems-Samples/blob/master/RecSys Traditional/MF/FM/fm_tensorflow.py](https://github.com/wyl6/Recommender-Systems-Samples/blob/master/RecSys Traditional/MF/FM/fm_tensorflow.py)
- [8] https://zhuanlan.zhihu.com/p/58160982
知识分享
个人知乎专栏:https://zhuanlan.zhihu.com/c_1164954275573858304
欢迎关注微信公众号:SOTA Lab
专注知识分享,不定期更新计算机、金融类文章
推荐系统系列(一):FM理论与实践的更多相关文章
- 计算广告CTR预估系列(七)--Facebook经典模型LR+GBDT理论与实践
计算广告CTR预估系列(七)--Facebook经典模型LR+GBDT理论与实践 2018年06月13日 16:38:11 轻春 阅读数 6004更多 分类专栏: 机器学习 机器学习荐货情报局 版 ...
- 推荐系统系列(四):PNN理论与实践
背景 上一篇文章介绍了FNN [2],在FM的基础上引入了DNN对特征进行高阶组合提高模型表现.但FNN并不是完美的,针对FNN的缺点上交与UCL于2016年联合提出一种新的改进模型PNN(Produ ...
- Java 理论与实践: 流行的原子——新原子类是 java.util.concurrent 的隐藏精华(转载)
简介: 在 JDK 5.0 之前,如果不使用本机代码,就不能用 Java 语言编写无等待.无锁定的算法.在 java.util.concurrent 中添加原子变量类之后,这种情况发生了变化.请跟随并 ...
- Java 理论和实践: 了解泛型
转载自 : http://www.ibm.com/developerworks/cn/java/j-jtp01255.html 表面上看起来,无论语法还是应用的环境(比如容器类),泛型类型(或者泛型) ...
- Java 理论与实践: 处理 InterruptedException(转)
很多 Java™ 语言方法,例如 Thread.sleep() 和 Object.wait(),都可以抛出InterruptedException.您不能忽略这个异常,因为它是一个检查异常(check ...
- DDD(领域驱动设计)理论结合实践
DDD(领域驱动设计)理论结合实践 写在前面 插一句:本人超爱落网-<平凡的世界>这一期,分享给大家. 阅读目录: 关于DDD 前期分析 框架搭建 代码实现 开源-发布 后记 第一次听 ...
- Java 理论与实践: 用弱引用堵住内存泄漏
弱引用使得表达对象生命周期关系变得容易了 虽然用 Java™ 语言编写的程序在理论上是不会出现“内存泄漏”的,但是有时对象在不再作为程序的逻辑状态的一部分之后仍然不被垃圾收集.本月,负责保障应用程序健 ...
- 高翔《视觉SLAM十四讲》从理论到实践
目录 第1讲 前言:本书讲什么:如何使用本书: 第2讲 初始SLAM:引子-小萝卜的例子:经典视觉SLAM框架:SLAM问题的数学表述:实践-编程基础: 第3讲 三维空间刚体运动 旋转矩阵:实践-Ei ...
- Java 理论与实践: 修复 Java 内存模型,第 2 部分(转载)
在 JSR 133 中 JMM 会有什么改变? 活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型(Java Memory Model, JMM)的公开建议.在本系列文章的 ...
随机推荐
- [转载]python with语句的用法
https://www.cnblogs.com/DswCnblog/p/6126588.html 看这篇文章的时候看到了python的类名()用法,很好奇,上网查了下,原来这就相当于对类进行实例化了. ...
- 使用 “Unicode 字符集 ” 使用错误,应该使用 “使用多字节字符集”
“void ATL::CStringT<BaseType,StringTraits>::Format(const wchar_t *,...)”: 不能将参数 1 从“const char ...
- webpack开启本地服务器与热更新
第一个webpack本地服务 webpack本地服务相关的一些操作指令与应用 一.第一个webpack本地服务 //工作区间 src//文件夹 index.js//入口文件 index.css//测试 ...
- Vue中的key到底有什么用?
key是为Vue中的vnode标记的唯一id,通过这个key,我们的diff操作可以更准确.更快速 diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行 ...
- 1如何给devexpress的gridview控件绘制全选按钮
1 首先注册gridview的this.edibandedGridView.CustomDrawColumnHeader += EdibandedGridView_CustomDrawColumnHe ...
- vue项目 时间戳转 格式
项目用了 element UI的日期插件,修改时 时间回显不了,打印出来是换行了,因此要转换 changeTime(value){ let date = new Date(value); let y ...
- SQLServer 主键插入
设置此命令后可以往主键插入值 set IDENTITY_INSERT 表名 on set IDENTITY_INSERT 表名 off 注意: 此语句是一个整体操作 反例: 先单步执行:set IDE ...
- C# 中 ContextMenuStrip 和 ContextMenu区别
简单来说,就是版本不同,只不过是升级后建议功能更加强大的ContextMenuStrip罢了,升级后的元件功能更强 . ContextMenu是VS2005里的,而ContextMenuStrip是V ...
- java调用ffmpeg获取视频文件信息的一些参数
一.下载ffmpeg http://www.ffmpeg.org/download.html 主要需要bin目录下的ffmpeg可执行文件 二.java代码实现 package com.aw.util ...
- SpringBoot-核心依赖说明
spring-boot-dependencies 一般用来放在父项目中,来声明依赖,子项目引入相关依赖而不需要指定版本号,好处就是解决依赖冲突,统一管理依赖版本号 利用pom的继承,一处声明,处处使用 ...