
这是一系列有关我作为.NET开发人员正在进行的进入
Kaggle竞赛的黑暗森林的旅程的系列文章。
在本文和以下文章中,我将专注于(几乎)纯神经网络。 这意味着,数据集准备工作的大部分无聊部分,例如填写缺失值,特征选择,离群值分析等。 将被有意跳过。
技术堆栈将是C#+ TensorFlow
tf.keras API。 从今天起,它还将需要Windows。 在以后的文章中,较大的型号可能需要合适的GPU来保持训练时间,以保持理智。
让我们预测房地产价格!
房价对于新手来说是一个巨大的竞争。 它的数据集很小,没有特殊规则,公共排行榜有很多参与者,您每天最多可以提交4个条目。
在Kaggle上注册,如果您还没有注册,请参加比赛,然后下载数据。 目标是预测test.csv中条目的销售价格(SalePrice列)。 存档中包含
train.csv ,其中约有1500个条目以已知的销售价格进行训练。 在进入神经网络之前,我们将从加载该数据集开始并进行一些探索。
分析训练数据
我是否说过我们将跳过数据集准备工作? 我撒谎了! 您必须至少看一次。令我惊讶的是,我没有找到在.NET标准类库中加载.csv文件的简便方法,因此我安装了一个名为
CsvHelper的NuGet软件包。 为了简化数据操作,我还获得了我最喜欢的LINQ扩展包
MoreLinq 。
将.csv数据加载到DataTable中static DataTable LoadData(string csvFilePath) { var result = new DataTable(); using (var reader = new CsvDataReader(new CsvReader(new StreamReader(csvFilePath)))) { result.Load(reader); } return result; }
ML.NET使用
DataTable训练数据操作实际上是一个坏主意。
ML.NET应该具有.csv加载功能,并且可以进行许多数据分类和浏览操作。 但是,当我刚进入房屋价格竞赛时,还没有为该特定目的做好准备。
数据如下所示(只有几行和几列):
编号 | MSSubClass | 鸣声 | LotFrontage | 洛塔雷亚 |
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不会自动推断列的数据类型,并假定其全部为字符串。 因此,下一步就是确定我们实际拥有的东西。 对于每一列,我计算了以下统计信息:不同值的数量,其中有多少个整数,以及有多少个浮点数(本文末尾将链接带有所有辅助方法的源代码):
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();
数值栏
事实证明,大多数列实际上是整数,但是由于神经网络大多数都在浮点数上起作用,因此无论如何我们都会将它们转换为双精度数。
分类栏
其他列描述了待售物业所属的类别。 它们都没有太多不同的值,这很好。 要将它们用作我们未来的神经网络的输入,它们也必须转换为
两倍 。
最初,我只是简单地将数字从0分配给distinctValueCount-1,但这并没有多大意义,因为实际上没有从“外观:蓝色”到“外观:绿色”到“外观:白色”的过渡。 因此,在早期,我将其更改为所谓的“
单热编码” ,其中每个唯一值都获得一个单独的输入列。 例如,“外观:蓝色”变为[1,0,0],“外观:白色”变为[0,0,1]。
让他们在一起
大量数据探索CentralAir:2个值,整数:0.00%,浮动:0.00%
街道:2个值,整数:0.00%,浮动数:0.00%
实用程序:2个值,整数:0.00%,浮点数:0.00%
胡同:3个值,整数:0.00%,浮点数:0.00%
BsmtHalfBath:3个值,整数:100.00%,浮点数:100.00%
HalfBath:3个值,ints:100.00%,floats:100.00%
LandSlope:3个值,整数:0.00%,浮动数:0.00%
PavedDrive:3个值,整数:0.00%,浮点数:0.00%
BsmtFullBath:4个值,整数:100.00%,浮点数:100.00%
ExterQual:4个值,整数:0.00%,浮点数:0.00%
壁炉:4个值,整数:100.00%,浮点数:100.00%
FullBath:4个值,ints:100.00%,floats:100.00%
GarageFinish:4个值,ints:0.00%,float:0.00%
KitchenAbvGr:4个值,整数:100.00%,浮点数:100.00%
KitchenQuality:4值,整数:0.00%,浮点数:0.00%
LandContour:4个值,整数:0.00%,浮动数:0.00%
LotShape:4个值,整数:0.00%,浮点数:0.00%
PoolQC:4个值,整数:0.00%,浮点数:0.00%
BldgType:5个值,整数:0.00%,浮点数:0.00%
BsmtCond:5个值,整数:0.00%,浮点数:0.00%
BsmtExposure:5个值,整数:0.00%,浮点数:0.00%
BsmtQual:5个值,整数:0.00%,浮点数:0.00%
ExterCond:5个值,整数:0.00%,浮点数:0.00%
围栏:5个值,整数:0.00%,浮动:0.00%
车库车:5个值,整数:100.00%,浮动车:100.00%
加热QC:5个值,整数:0.00%,浮点数:0.00%
LotConfig:5个值,整数:0.00%,浮点数:0.00%
MasVnrType:5个值,整数:0.00%,浮点数:0.00%
其他功能:5个值,整数:0.00%,浮点数:0.00%
MSZoning:5个值,整数:0.00%,浮点数:0.00%
已售:5个值,整数:100.00%,浮动数:100.00%
电气:6个值,整数:0.00%,浮点数:0.00%
FireplaceQu:6个值,整数:0.00%,浮点数:0.00%
基础:6个值,整数:0.00%,浮点数:0.00%
GarageCond:6个值,整数:0.00%,浮点数:0.00%
GarageQual:6个值,整数:0.00%,浮点数:0.00%
加热:6个值,整数:0.00%,浮动:0.00%
RoofStyle:6个值,整数:0.00%,浮点数:0.00%
Sale条件:6个值,整数:0.00%,浮动数:0.00%
BsmtFinType1:7个值,整数:0.00%,浮点数:0.00%
BsmtFinType2:7个值,整数:0.00%,浮点数:0.00%
功能:7个值,整数:0.00%,浮点数:0.00%
GarageType:7个值,整数:0.00%,浮点数:0.00%
BedroomAbvGr:8个值,整数:100.00%,浮点数:100.00%
Condition2:8个值,整数:0.00%,浮点数:0.00%
HouseStyle:8个值,整数:0.00%,浮点数:0.00%
PoolArea:8个值,ints:100.00%,floats:100.00%
RoofMatl:8个值,整数:0.00%,浮点数:0.00%
Condition1:9个值,整数:0.00%,浮点数:0.00%
TotalCond:9个值,ints:100.00%,floats:100.00%
SaleType:9个值,整数:0.00%,浮动数:0.00%
综合质量:10个值,整数:100.00%,浮点数:100.00%
MoSold:12个值,ints:100.00%,floats:100.00%
TotRmsAbvGrd:12个值,整数:100.00%,浮点数:100.00%
外部第一个:15个值,整数:0.00%,浮点数:0.00%
MSSubClass:15个值,整数:100.00%,浮点数:100.00%
Exterior2nd:16个值,整数:0.00%,浮点数:0.00%
3SsnPorch:20个值,整数:100.00%,浮动数:100.00%
MiscVal:21个值,整数:100.00%,浮点数:100.00%
LowQualFinSF:24个值,整数:100.00%,浮点数:100.00%
邻里:25个值,整数:0.00%,浮动:0.00%
YearRemodAdd:61个值,整数:100.00%,浮点数:100.00%
ScreenPorch:76个值,ints:100.00%,floats:100.00%
GarageYrBlt:98个值,整数:94.45%,浮点数:94.45%
LotFrontage:111个值,ints:82.26%,floats:82.26%
YearBuilt:112个值,ints:100.00%,floats:100.00%
封闭通道:120个值,整数:100.00%,浮点数:100.00%
BsmtFinSF2:144个值,整数:100.00%,浮点数:100.00%
OpenPorchSF:202个值,整数:100.00%,浮点数:100.00%
WoodDeckSF:274个值,整数:100.00%,浮动:100.00%
MasVnrArea:328个值,整数:99.45%,浮动数:99.45%
2ndFlrSF:417个值,整数:100.00%,浮点数:100.00%
GarageArea:441个值,整数:100.00%,浮动:100.00%
BsmtFinSF1:637个值,整数:100.00%,浮点数:100.00%
售价:663个值,整数:100.00%,流通量:100.00%
TotalBsmtSF:721个值,整数:100.00%,浮点数:100.00%
1stFlrSF:753个值,整数:100.00%,浮点数:100.00%
BsmtUnfSF:780个值,整数:100.00%,浮点数:100.00%
GrLivArea:861值,整数:100.00%,浮点数:100.00%
LotArea:1073值,ints:100.00%,floats:100.00%
许多值列:
Exterior1st:AsbShng,AsphShn,BrkComm,BrkFace,CBlock,CemntBd,HdBoard,ImStucc,MetalSd,胶合板,石材,灰泥,VinylSd,Wd Sdng,WdShing
Exterior2nd:AsbShng,AsphShn,Brk Cmn,BrkFace,CBlock,CmentBd,HdBoard,ImStucc,MetalSd,其他,胶合板,石材,灰泥,VinylSd,Wd Sdng,Wd Shng
社区:Blmngtn,Blueste,BrDale,BrkSide,ClearCr,CollgCr,Crawfor,Edwards,Gilbert,IDOTRR,MeadowV,Mitchel,NAmes,NoRidge,NPkVill,NridgHt,NWAmes,OldTown,Sawyer,SawyerW,Somerst,Stone,Stoner文克
不可解析的浮点数
GarageYrBlt:不适用
LotFrontage:不适用
MasVnr地区:不适用
浮动范围:
BsmtHalfBath:0 ... 2
半浴:0 ... 2
BsmtFullBath:0 ... 3
壁炉:0 ... 3
全浴:0 ... 3
KitchenAbvGr:0 ... 3
车库车:0 ... 4
已售:2006年... 2010年
BedroomAbvGr:0 ... 8
泳池面积:0 ... 738
总体状况:1 ... 9
综合素质:1 ... 10
MoSold:1 ... 12
TotRmsAbvGrd:2 ... 14
MS子类:20 ... 190
3SsnPorch:0 ... 508
杂项Val:0 ... 15500
LowQualFinSF:0 ... 572
年改款年份:1950年至2010年
屏幕通道:0 ... 480
GarageYrBlt:1900 ... 2010
前额:21 ... 313
建成年份:1872年至2010年
封闭门廊:0 ... 552
BsmtFinSF2:0 ... 1474年
OpenPorchSF:0 ... 547
木甲板SF:0 ... 857
MasVnrArea:0 ... 1600
2ndFlrSF:0至2065
车库面积:0 ... 1418
BsmtFinSF1:0 ... 5644
销售价格:34,900 ... 755,000
TotalBsmtSF:0 ... 6110
1stFlrSF:334 ... 4692
BsmtUnfSF:0 ... 2336
GrLivArea:334 ... 5642
LotArea:1300 ... 215245
考虑到这一点,我构建了以下
ValueNormalizer ,它获取有关列内值的一些信息,并返回一个函数,该函数将值(
字符串 )转换为神经网络的数字特征向量(
double [] ):
ValueNormalizer 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); } }
现在,我们已经将数据转换为适用于神经网络的格式。 现在是时候建立一个了。
建立神经网络
从今天开始,您将需要使用Windows计算机。如果您已经安装了Python和TensorFlow 1.1x,那么您所需要做的就是
<PackageReference Include="Gradient" Version="0.1.10-tech-preview3" />
在您的现代.csproj文件中。 否则,请参考“
渐变”手册进行初始设置。
程序包启动并运行后,我们可以创建第一个浅层深度网络。
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个神经元层和一个辍学层的未经训练的神经网络,有助于防止过度拟合。
tf.nn.relu_fntf.nn.relu_fn是我们神经元的激活功能。 众所周知,
ReLU在深度网络中能很好地工作,因为它解决了
消失的梯度问题 :当误差从深度网络中的输出层传播回去时,原始非线性激活函数的导数往往变得非常小。 这意味着,靠近输入的层仅会进行很小的调整,这会大大减慢对深度网络的训练。
辍学辍学是神经网络中的特殊功能层,实际上不包含神经元。 取而代之的是,它通过获取每个单独的输入进行操作,并在自身输出上将其随机替换为0(否则,它只会传递原始值)。 这样可以防止在较小的数据集中
过度拟合不太相关的特征。 例如,如果我们不删除
Id列,则网络可能准确地存储了<Id>-> <SalePrice>映射,这将为我们提供100%的训练集准确性,但在其他任何数据上却完全不相关。
为什么我们需要辍学? 我们的训练数据只有1500个示例,而我们构建的这个小型神经网络具有1800多个可调权重。 如果它是一个简单的多项式,则可以与我们试图精确逼近的价格函数匹配。 但是在原始训练集之外的任何输入上,它都将具有巨大的价值。
送入数据
TensorFlow期望其数据以NumPy数组或现有张量存在。 我将DataRows转换为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);
在上面的代码中,我们通过获取其中的每个单元格并应用与其列对应的
ValueNormalizer ,将每个
DataRow转换为
ndarray 。 然后,将所有行放入另一个
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,预测值对的列表。
提交结果时,您应获得0.17的分数,该分数将位于公共排行榜表的最后四分之一位置。 但是,如果它像具有27个神经元的3层网络那样简单,那么那些讨厌的数据科学家将不会从美国主要公司那里得到30万美元/年的总补偿。
总结
此条目的完整源代码(包括所有助手,以及我先前的探索和实验中一些注释掉的部分)在
PasteBin上大约有200行。
在下一篇文章中,您将看到我的恶作剧者试图进入该公共排行榜的前50%。 这将是业余爱好者的冒险,是与流浪者唯一的工具与过拟合的风车之战-更大的模型(例如,深度神经网络,请记住,没有手动特征工程!)。 这将不再是编码教程,而是更多的思想探索,其中包含真正的歪曲数学和怪异的结论。 敬请期待!
友情链接
卡格勒Kaggle的房价竞争TensorFlow回归教程TensorFlow主页TensorFlow API参考渐变(TensorFlow绑定)