.NET、TensorFlow 和 Kaggle 的风车






4.95/5 (15投票s)
.NET 上的 TensorFlow 数据科学实战竞赛
这是关于我作为一名 .NET 开发者,在 Kaggle 竞赛的黑暗森林中持续探索的系列文章。
在本文及后续文章中,我将专注于(几乎)纯神经网络。这意味着,像填充缺失值、特征选择、异常值分析等数据准备中大部分枯燥的部分将被有意忽略。
技术栈将是 C# + TensorFlow tf.keras API。截至今天,还需要 Windows。未来文章中的大型模型可能需要合适的 GPU 来保持其训练时间的可行性。
让我们来预测房地产价格!
房屋价格 是一个非常适合新手入门的竞赛。其数据集很小,没有特殊规则,公共排行榜有很多参与者,而且您每天最多可以提交 4 次。
如果您还没有注册 Kaggle,请立即注册,加入此竞赛,并下载数据。目标是预测 test.csv 中条目的销售价格(SalePrice
列)。存档包含 train.csv,其中有大约 1500 个具有已知销售价格的条目可供训练。在深入神经网络之前,我们将从加载该 数据集
并对其进行一些探索开始。
分析训练数据
我说过要跳过数据准备?我撒谎了!您至少要看一次。
令我惊讶的是,我在 .NET 标准类库中没有找到简单的方法来加载 .csv 文件,所以我安装了一个名为 CsvHelper 的 NuGet 包。为了简化数据操作,我还安装了我新喜欢的 LINQ 扩展包 MoreLinq。
static DataTable LoadData(string csvFilePath) {
var result = new DataTable();
using (var reader = new CsvDataReader(new CsvReader(new StreamReader(csvFilePath)))) {
result.Load(reader);
}
return result;
}
使用 DataTable
来处理训练数据实际上是一个糟糕的主意。
ML.NET 应该具备 .csv 加载以及许多数据准备和探索操作。然而,当我刚开始参加房屋价格竞赛时,它还没有为此特定目的做好准备。
数据看起来是这样的(只有几行和几列)
ID | MSSubClass | MSZoning | LotFrontage | LotArea |
1 | 60 | RL | 65 | 8450 |
2 | 20 | RL | 80 | 9600 |
3 | 60 | RL | 68 | 11250 |
4 | 70 | RL | 60 | 9550 |
加载数据后,我们需要删除 Id
列,因为它与房屋价格实际上无关。
var trainData = LoadData("train.csv");
trainData.Columns.Remove("Id");
分析列数据类型
DataTable
不会自动推断列的数据类型,它假定所有都是 string
。因此,下一步是确定我们实际上有什么。对于每一列,我计算了以下统计信息:不同值的数量、其中有多少是整数,以及有多少是浮点数(所有辅助方法的源代码将在文章末尾链接)。
var values = rows.Select(row => (string)row[column]);
double floats = values.Percentage(v => double.TryParse(v, out _));
double ints = values.Percentage(v => int.TryParse(v, out _));
int distincts = values.Distinct().Count();
数值列
事实证明,大多数列实际上是 int
,但由于神经网络主要处理浮点数,我们无论如何都会将它们转换为 double
。
分类列
其他列描述了待售房产所属的类别。它们中没有太多不同的值,这很好。为了将它们用作未来神经网络的输入,它们也必须转换为 double
。
最初,我简单地将数字从 0
分配给 distinctValueCount - 1
,但这没有多大意义,因为实际上从“Facade: Blue
”到“Facade: Green
”再到“Facade: White
”并没有递进关系。因此,我早就将其更改为所谓的 独热编码,其中每个唯一值都有一个单独的输入列。例如,“Facade: Blue
”变为 [1,0,0]
,而“Facade: White
”变为 [0,0,1]
。
将它们全部放在一起
CentralAir: 2 values, ints: 0.00%, floats: 0.00%
Street: 2 values, ints: 0.00%, floats: 0.00%
Utilities: 2 values, ints: 0.00%, floats: 0.00%
....
LotArea: 1073 values, ints: 100.00%, floats: 100.00%
Many value columns:
Exterior1st: AsbShng, AsphShn, BrkComm, BrkFace, CBlock, CemntBd, HdBoard,
ImStucc, MetalSd, Plywood, Stone, Stucco, VinylSd, Wd Sdng, WdShing
Exterior2nd: AsbShng, AsphShn, Brk Cmn, BrkFace, CBlock, CmentBd, HdBoard,
ImStucc, MetalSd, Other, Plywood, Stone, Stucco, VinylSd, Wd Sdng, Wd Shng
Neighborhood: Blmngtn, Blueste, BrDale, BrkSide, ClearCr, CollgCr, Crawfor,
Edwards, Gilbert, IDOTRR, MeadowV, Mitchel, NAmes, NoRidge, NPkVill,
NridgHt, NWAmes, OldTown, Sawyer, SawyerW, Somerst,
StoneBr, SWISU, Timber, Veenker
non-parsable floats
GarageYrBlt: NA
LotFrontage: NA
MasVnrArea: NA
float ranges:
BsmtHalfBath: 0...2
HalfBath: 0...2
...
GrLivArea: 334...5642
LotArea: 1300...215245
考虑到这一点,我构建了以下 ValueNormalizer
,它接受有关列内值的一些信息,并返回一个函数,该函数将一个值(一个 string
)转换为神经网络的数值特征向量(double[]
)。
static Func<string, double[]> ValueNormalizer(double floats, IEnumerable<string> values) {
if (floats > 0.01) {
double max = values.AsDouble().Max().Value;
return s => new[] { double.TryParse(s, out double v) ? v / max : -1 };
} else {
string[] domain = values.Distinct().OrderBy(v => v).ToArray();
return s => new double[domain.Length+1]
.Set(Array.IndexOf(domain, s)+1, 1);
}
}
现在,我们已经将数据转换为适合神经网络的格式。是时候构建一个了。
构建神经网络
如果您已经安装了 Python 3.6 和 TensorFlow 1.10.x,您只需要
<PackageReference Include="Gradient" Version="0.1.10-tech-preview4" />
在您的现代 .csproj 文件中。否则,请参考 Gradient 手册 进行初始设置。
一旦包启动并运行,我们就可以创建我们的第一个浅层深度网络。
using tensorflow;
using tensorflow.keras;
using tensorflow.keras.layers;
using tensorflow.train;
...
var model = new Sequential(new Layer[] {
new Dense(units: 16, activation: tf.nn.relu_fn),
new Dropout(rate: 0.1),
new Dense(units: 10, activation: tf.nn.relu_fn),
new Dense(units: 1, activation: tf.nn.relu_fn),
});
model.compile(optimizer: new AdamOptimizer(), loss: "mean_squared_error");
这将创建一个未训练的神经网络,包含 3 个神经元层和一个 dropout 层,有助于防止过拟合。
tf.nn.relu_fn 是我们神经元的激活函数。 ReLU 以在深度网络中表现良好而闻名,因为它解决了 梯度消失问题:在深度网络中,当误差从输出层反向传播时,原始非线性激活函数的导数往往会变得非常小。这意味着靠近输入层的层只会进行微小的调整,这极大地减缓了深度网络的训练速度。
Dropout 是神经网络中的一种特殊功能层,它实际上本身不包含神经元。相反,它的工作方式是接受每个单独的输入,并随机将其输出替换为 0
(否则,它只是传递原始值)。通过这样做,它有助于防止在小型 dataset
中对不太相关的特征 过拟合。例如,如果我们没有删除 Id
列,网络可能已经精确地记住了 <Id>
-><SalePrice>
的映射,这将在训练集上给我们带来 100% 的准确率,但在任何其他数据上都会产生完全不相关的数字。我们为什么需要 dropout?我们的训练数据只有大约 1500 个样本,而我们构建的这个微型神经网络有 > 1800 个可调权重。如果它是一个简单的多项式,它可以精确匹配我们试图逼近的价格函数。但是,它会在原始训练集之外的任何输入上产生巨大的值。
馈送数据
TensorFlow 期望其数据是 NumPy 数组或现有张量。我正在将 DataRow
转换为 NumPy 数组。
using numpy;
...
const string predict = "SalePrice";
ndarray GetInputs(IEnumerable<DataRow> rowSeq) {
return np.array(rowSeq.Select(row => np.array(
columnTypes
.Where(c => c.column.ColumnName != predict)
.SelectMany(column => column.normalizer(
row.Table.Columns.Contains(column.column.ColumnName)
? (string)row[column.column.ColumnName]
: "-1"))
.ToArray()))
.ToArray()
);
}
var predictColumn = columnTypes.Single(c => c.column.ColumnName == predict);
ndarray trainOutputs = np.array(predictColumn.trainValues
.AsDouble()
.Select(v => v ?? -1)
.ToArray());
ndarray trainInputs = GetInputs(trainRows);
在上面的代码中,我们将每个 DataRow
转换为一个 ndarray
,方法是获取其中的每个单元格,并应用与其列相对应的 ValueNormalizer
。然后,我们将所有行放入另一个 ndarray
中,得到一个数组的数组。
输出不需要任何此类转换,我们只需将训练值转换为另一个 ndarray
。
是时候开始梯度下降了
有了这个设置,要训练我们的网络,我们只需要调用模型的 fit
函数。
model.fit(trainInputs, trainOutputs,
epochs: 2000,
validation_split: 0.075,
verbose: 2);
此调用实际上会将最后 7.5% 的训练集用于验证,然后重复以下 2000 次:
- 将剩余的
trainInputs
分成批次 - 逐个将这些批次馈送到神经网络
- 使用我们上面定义的损失函数计算误差
- 通过单个神经元连接的梯度反向传播误差,调整权重
在训练过程中,它将输出网络在用于验证的数据上的误差(val_loss
)以及网络在训练数据本身上的误差(loss
)。通常,如果 val_loss
远大于 loss
,则表示网络开始过拟合。我将在后续文章中更详细地讨论这一点。
如果您一切都做得正确,您其中一个损失值的平方根应该在 20000 左右。
提交
我在这里不会详细介绍如何生成要提交的文件。计算输出的代码很简单。
const string SubmissionInputFile = "test.csv";
DataTable submissionData = LoadData(SubmissionInputFile);
var submissionRows = submissionData.Rows.Cast<DataRow>();
ndarray submissionInputs = GetInputs(submissionRows);
ndarray sumissionOutputs = model.predict(submissionInputs);
它主要使用前面定义的函数。
然后您需要将它们写入一个 .csv 文件,该文件只是 Id
、predicted_value
对的列表。
当您提交结果时,您应该会获得一个大约 0.17
的分数,这大约在公共排行榜表格的最后四分之一。但嘿,如果像一个拥有 27 个神经元的 3 层网络这么简单,那些讨厌的数据科学家就不会从美国主要公司获得每年 30 万美元以上的总薪酬了。
总结
本文的完整源代码(包含所有辅助函数以及我早期探索和实验的一些注释掉的部分)在 PasteBin 上,大约有 200 行。
在下一篇文章中,您将看到我为了进入公共排行榜前 50% 而进行的折腾。这将是一场业余学徒的冒险,一场与过拟合之风车搏斗的战斗,而游荡者拥有的唯一工具——一个更大的模型(例如,深度神经网络,记住,没有手动特征工程!)。这与其说是一篇编码教程,不如说是一次思维探索,包含非常粗糙的数学和一个奇怪的结论。
敬请期待!
链接
历史
- 2019 年 2 月 23 日:初始版本