从零开始跑通 TensorFlow 的第一个模型其实是件挺考验耐心的事。我见过不少朋友兴致勃勃装了环境结果卡在“先学什么模型”这个问题上翻了一堆资料从卷积神经网络看到 Transformer最后还是回到最简单的线性回归。这个选择其实很明智TensorFlow 里的高阶 API 封装程度很高如果一上来就追热点模型很容易被复杂的张量操作和训练细节劝退。反而是从线性回归和逻辑回归入手能把“数据流图、损失函数、梯度下降、评估指标”这条主线一次理清之后再去看 CNN、RNN 就会顺畅很多。这篇内容就是沿着这条主线写的定位是给刚接触深度学习的开发者看的手把手指南。我会先讲讲为什么选 TensorFlow然后分别拆解线性回归和逻辑回归的数学原理、代码实现、训练过程最后说一说我把这两个模型跑通之后对后续学习方向的一些体会。整个过程里我会把环境配置、数据准备、训练循环这些容易被忽略的细节也一并交代清楚你可以直接照着敲代码应该能在半天内把这两个经典模型完整跑通。1. TensorFlow 到底是什么以及为什么我推荐用它入门1.1 TensorFlow 能做什么它解决的痛点是什么TensorFlow 本质上是一个数值计算框架它把复杂的数学运算组织成一张有向图图中的节点是操作op边是数据张量。你在写模型时定义的网络结构会被转换成这张计算图然后交给底层的执行引擎去调度充分利用 CPU、GPU 甚至 TPU 的计算能力。对我来说它最大的价值不是“跑得快”而是“思路清晰”——你把前向传播搭好声明好损失函数剩下的事情框架帮你安排。很多人觉得深度学习很神秘其实核心就三件事定义模型结构、定义损失函数、定义优化方法。这三个步骤 TensorFlow 都提供了高度封装的接口。框架本身是一个工具箱而线性回归和逻辑回归就是你第一次真正使用这个工具箱去解决实际问题的尝试。TensorFlow 2.x 之后默认开启了 Eager Execution动态图模式你可以像写普通 Python 一样逐行执行张量操作不用像早期版本那样先构建图再统一运行调试体验好了非常多。1.2 版本选择和环境配置的几条经验早期 TensorFlow 1.x 有tf.Session()、tf.placeholder这些概念现在 2.x 基本已经不用了。2024 年之后新入门的朋友我建议不要去看 1.x 的旧教程直接学 2.x 的 Keras API 就好。顺便说一句如果你是从 PyTorch 转过来的会发现 TensorFlow 2.x 的写法其实已经没那么别扭了。环境配置给两条建议。第一用虚拟环境隔离项目依赖不要直接装到系统 Python 里排除包冲突的隐患。第二新手阶段用 CPU 版本跑这两个模型完全够了线性回归和逻辑回归都是毫秒级到秒级的任务没必要为了跑 demo 折腾 GPU 驱动和 CUDA。直接一行pip install tensorflow即可。注意如果你的机器是 Apple Silicon 芯片推荐安装tensorflow-macos加上tensorflow-metal插件CPU 版本的训练速度也能有不错的提升。Windows 用户直接装标准版就行。2. 线性回归从原理到代码一步步拆解2.1 线性回归的数学直觉拟合一条线线性回归要解决的问题很朴素给你一堆散点让你找一条直线尽可能穿过它们。比如你想根据房子面积预测房价假设数据大致呈线性关系那模型就是一个形如y w * x b的一次函数。这里的w是权重b是偏置两个都是变量模型训练的目标就是找到一组最好的w和b让预测值跟真实值之间的差距最小。怎么衡量“差距”最常用的是均方误差MSE公式是每个样本预测值与真实值差值的平方和除以样本数。平方的目的是消除正负号抵消、同时放大大误差的影响——误差越大平方后惩罚越重。为什么用平方而不是绝对值因为平方函数是处处可导且光滑的方便后面做梯度下降。你用绝对值的话在零点处不可导优化过程容易卡住。2.2 手写核心训练循环理解底层的梯度更新过程在跳进 Keras 高层 API 之前我强烈建议你先手写一次训练循环哪怕只跑一次也好。这样才能真正理解“训练”发生了什么。import numpy as np import tensorflow as tf # 生成模拟数据y 3x 2 噪声 x_data np.random.rand(200, 1).astype(np.float32) y_data 3 * x_data 2 0.1 * np.random.randn(200, 1).astype(np.float32) # 定义可训练变量 w tf.Variable(tf.random.normal([1, 1])) b tf.Variable(tf.zeros([1])) learning_rate 0.01 for epoch in range(500): with tf.GradientTape() as tape: # 前向传播 y_pred tf.matmul(x_data, w) b # 计算损失 loss tf.reduce_mean(tf.square(y_data - y_pred)) # 反向传播自动求梯度 grads tape.gradient(loss, [w, b]) # 手动更新参数 w.assign_sub(learning_rate * grads[0]) b.assign_sub(learning_rate * grads[1]) if epoch % 100 0: print(fEpoch {epoch}, Loss: {loss.numpy():.4f}) print(f训练结果: w {w.numpy()}, b {b.numpy()})这段代码有几个地方值得细看。tf.GradientTape()是 TensorFlow 2.x 的自动求导机制它会记录在它作用域内的所有张量操作然后调用tape.gradient()就能自动计算损失对每个变量的梯度。这背后用到了链式法则但你不必手动推导框架都帮你做了。训练的本质就是“猜w和b算损失求梯度沿着梯度反方向更新参数”。assign_sub就是做w w - learning_rate * gradient这个操作它是在原变量上原地更新而不是重新创建变量这样可以保证计算图里的连接关系不变。学习率0.01是我试了试比较合适的值太大了会震荡不收敛太小了收敛慢后面讲问题排查时再展开。2.3 用 tf.keras 高层 API 实现真正的生产写法手写循环理解原理但实际项目中我更推荐用 Keras 的Sequential模型代码精简很多可读性也好。Keras 把前向传播、损失计算、梯度更新都封装好了你只需要几行声明式的代码。model tf.keras.Sequential([ tf.keras.layers.Dense(1, input_shape(1,)) ]) model.compile(optimizersgd, lossmse, metrics[mae]) history model.fit(x_data, y_data, epochs200, verbose1)Dense(1)就是全连接层它做的事情就是y w * x b跟前面手写的完全一样。input_shape(1,)告诉模型输入是一个一维特征。optimizersgd就是随机梯度下降lossmse对应均方误差metrics[mae]会额外输出平均绝对误差方便你直观理解预测偏差的量级。fit函数是整个训练过程的入口它内部帮你处理了批次切分、梯度更新、日志打印等所有细节。训练完成后可以调用模型查看参数w_trained, b_trained model.layers[0].get_weights() print(fKeras 训练结果: w {w_trained[0][0]}, b {b_trained[0]})我实测下来手写循环和 Keras 高层 API 训练出的参数基本一致误差在小数点后几位。区别在于 Keras 帮你做了更多工程化处理比如自动切分 batch、每个 epoch 的进度条、验证集评估等这些在真实项目中都是必须的。3. 逻辑回归分类问题从这里开始3.1 从回归到分类逻辑回归到底“回归”了什么逻辑回归名字里带着“回归”实际上解决的是分类问题。它的思路是在线性回归的输出外面套一层激活函数sigmoid把任意实数压缩到 0 到 1 之间输出可以解释为样本属于某个类别的概率。比如判断一封邮件是否是垃圾邮件输出是 0.8就可以说“80% 的概率是垃圾邮件”。sigmoid 函数公式是sigmoid(z) 1 / (1 exp(-z))z 是线性部分w * x b的输出。这个函数有几个好性质值域在 (0,1)单调递增且是可导的方便梯度计算。它的导数有个很漂亮的形式sigmoid(z) * (1 - sigmoid(z))这让反向传播的计算变得非常简洁这也是为什么它成为经典激活函数的原因。这里面有个关键点分类问题不能再用均方误差作为损失函数了。逻辑回归用的是二元交叉熵binary crossentropy公式是-y * log(y_pred) - (1 - y) * log(1 - y_pred)。为什么换损失函数因为 sigmoid 函数会在两端饱和如果还用均方误差梯度会变得很小模型学不动收敛极慢。交叉熵在概率输出场景下梯度更稳定优化曲面更适合梯度下降。3.2 用 tf.keras 实现二分类模型二分类问题的标准做法是最后一层用含 1 个神经元的 Dense 层激活函数用 sigmoid损失函数用 binary_crossentropy。我以“模拟某销售系统判断用户是否会产生消费”为例写一个完整的流程。import numpy as np import tensorflow as tf # 造一份二分类模拟数据 np.random.seed(42) n_samples 1000 # 两个特征访问时长和页面浏览数 X np.random.randn(n_samples, 2).astype(np.float32) # 生成标签简单的线性可分 一点噪声 y (X[:, 0] * 2 X[:, 1] * 1 0.5 0).astype(np.float32) # 切分训练集和测试集 split int(n_samples * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] model tf.keras.Sequential([ tf.keras.layers.Dense(1, activationsigmoid, input_shape(2,)) ]) model.compile(optimizertf.keras.optimizers.Adam(learning_rate0.01), lossbinary_crossentropy, metrics[accuracy]) history model.fit(X_train, y_train, epochs100, batch_size32, validation_data(X_test, y_test), verbose1) # 评估测试集 loss, acc model.evaluate(X_test, y_test, verbose0) print(f测试集准确率: {acc:.4f}) # 预测 probs model.predict(X_test[:5]) print(预测概率:, probs.flatten())这里我特意用了 Adam 优化器而不是 SGD。Adam 能根据每个参数的历史梯度自动调整学习率对新手来说不用手动调学习率就能跑出不错的结果省心很多。batch_size32表示每 32 个样本做一次梯度更新训练时内存占用小、且梯度估计更稳。validation_data参数会在每个 epoch 结束后在验证集上评估模型这样你能实时看到模型在没见过数据上的表现防止过拟合。3.3 多分类问题的那些“进阶”细节如果你拿来训练的数据不是两个类别而是手写数字识别那种 10 分类逻辑回归的思想可以无缝扩展输出层神经元数量改成类别数激活函数换成 softmax损失函数换成 categorical_crossentropy。softmax 做的事情是把一组实数logits变成一组和为 1 的概率分布最大的那个就是模型预测的类别。这里有个编码细节容易踩坑categorical_crossentropy要求标签是 one-hot 编码比如 [0,1,0,0...]而sparse_categorical_crossentropy可以直接接受整数标签比如 2 就代表第 2 类。实际用的时候要对应好否则会报形状不匹配的错误。我个人习惯用sparse_categorical_crossentropy因为多数数据集加载出来是整数标签省一个编码步骤。多分类跟二分类的区别还有一个点输出层没有用 sigmoid用的是 softmax。两者本质都是把线性输出映射为概率区别在于 softmax 保证所有类别概率之和为 1且每个概率都大于 0。这种设计天然适合互斥分类场景。4. 从线性到非线性这两个模型学习价值的关键所在4.1 两个模型的本质关联以及它们之间的经典延展线性回归和逻辑回归的关系可以理解为一个连续的递进线性回归输出连续实数值用于预测逻辑回归在线性输出的基础上加非线性变换输出离散概率用于分类。它们在数学结构上高度统一区别只在损失函数和最后的激活层。掌握了这两个模型后续学神经网络的路就很顺了。教科书上经典的“单层感知机”本质上就是一个逻辑回归模型而深层神经网络就是堆叠了多个这样的层并且在每层之间加入非线性激活函数。你可以把这两个模型理解为深度学习的“原子组件”。从这两个模型里收获的建模思路——定义结构、选损失、优化求解——和后面搭 CNN、RNN 时的思路完全一致。4.2 为什么“会写代码”还不够关键是理解数据流我指导过一些朋友代码能跑通、结果也对但换一个数据集就不会改代码了。问题往往出在没搞懂数据形状流经网络的路径。在 Keras 里每层张量的形状变化比如输入是(样本数, 特征数)经过Dense(1)变成(样本数, 1)再和标签(样本数, 1)算损失。这个形状流如果不清楚改输入特征数量时必然报错。建议你可以做一个小实验定义一个很简单的模型用model.summary()打印网络结构再用model.layers[i].output观察中间张量的形状。这个习惯比背 API 更有价值因为深度学习里 80% 的报错都是形状不匹配导致的。运行一下model.summary()看看输出Model: sequential _________________________________________________________________ Layer (type) Output Shape Param # dense (Dense) (None, 1) 3 这里的None表示 batch 维度可以是任意大小。Param 是 3对应两个特征的权重加一个偏置。这个信息对排查问题很重要。5. 常见报错排查新手最容易踩的坑5.1 形状不匹配问题报错信息里最常见的就是ValueError: Shapes (None, 1) and (None, 2) are incompatible。我举个例子你的标签 y 如果是二维的(样本数, 1)而模型输出的形状在某些配置下变成了(样本数,)两者做损失计算就会报错。排查思路很简单打印x.shape和y.shape确认维度再用reshape或tf.squeeze统一形状。还有种情况是 Keras 里当你用Sequential的第一层不指定input_shape时模型会在第一次fit时推断输入形状。如果训练数据和验证数据的特征数不一致fit 过程中会直接报警。所以检查数据形状是第一步别急着改模型。5.2 损失值不下降或变成 NaN 的处理损失值一直不降最常见的原因是学习率太大或数据没做归一化。像线性回归里特征如果跨了几个数量级房价上百万、面积几十平梯度更新的方向就会扭曲。解决办法是数据标准化减均值除以标准差让特征大致落在同一个尺度上。损失变成 NaN多半是梯度爆炸了。针对逻辑回归我遇到过一种典型情况学习率设成 0.1adam 优化器也救不回来损失直接是 nan。解决方式是降低学习率或者对输入数据做归一化。还有个隐性坑是标签数据里有 NaN这种一般来自数据预处理阶段可以用np.isnan(y).any()检查一下。注意遇到 NaN先查数据再查学习率。不要一上来就怀疑模型结构线性回归和逻辑回归的结构极其简单很少是结构本身出问题。5.3 过拟合和欠拟合的快速判断训练集 loss 很低、验证集 loss 很高这是过拟合。线性回归模型一般不会过拟合因为模型容量太小。逻辑回归也很少过拟合但如果你加了过多高阶特征比如把 x 变成 x^2、x^3就会有这个问题。判断方法就一个看验证集指标是否明显差于训练集。反过来如果训练集 loss 就很高那是欠拟合说明模型容量不够或者数据分布不是线性的。这时候不要硬调参数而是考虑换非线性模型。我见过有人在线性回归上死磕调了一天学习率结果数据本身就是二次函数线性模型怎么可能拟合得好遇到这种情况直接上多项式特征或换模型才是正解。6. 跑通这两个模型之后下一步怎么走我个人觉得学深度学习最好的节奏是“一次只往前迈一步”。线性回归和逻辑回归跑通之后可以尝试这样几个自然延伸把逻辑回归的 2 分类扩展到 10 分类的多分类任务体验 softmax 和交叉熵的组合把线性回归的单一特征改成多个特征体会输入维度对模型结构和训练速度的影响再进一步把网络加深一层比如Dense(8, activationrelu)加Dense(1)你就能看到引入非线性激活函数后模型对非线性数据的拟合能力有了质的飞跃这时候你再回头看今天的两个模型就能真正看到深度学习层层递进的全貌。我在给一些初学者做指导时经常强调一件事不要急着追新模型新架构把最简单的模型吃透比看十篇热门论文有用得多。因为你会在实践中建立起“模型结构 — 损失函数 — 数据分布 — 优化过程”这个完整的思维闭环。有了这个闭环后面遇到的每个模型、每个新架构你都能快速抓住它的核心。最后再分享一个小技巧跑模型时把model.fit的返回值history保存下来用 matplotlib 画一下训练过程的 loss 曲线。我每次调试模型都会先看这个曲线它比任何日志都直观。Loss 曲线如果下降平滑说明训练状态正常如果剧烈震荡说明学习率太激进如果验证集 loss 不掉反升过拟合的信号很明显。这个习惯陪我排查了非常多的模型问题建议你也一试。