当前位置: 首页 > news >正文

由从零开始的神经网络理解torch的几个模块

从零开始的神经网络

        PyTorch 提供设计精美的模块和类torch.nn,torch.optim,Dataset和DataLoader神经网络。 为了充分利用它们的功能并针对您的问题对其进行自定义,您需要真正了解它们在做什么。 为了建立这种理解,我们将首先在 MNIST 数据集上训练基本神经网络,而无需使用这些模型的任何功能。 我们最初将仅使用最基本的 PyTorch 张量函数。 然后,我们将一次从torch.nntorch.optimDatasetDataLoader中逐个添加一个函数,以准确显示每个函数,以及如何使代码更简洁或更有效。 灵活。

导入MNIST数据集

        我们将使用经典的 MNIST 数据集,该数据集由手绘数字的黑白图像组成(0 到 9 之间)。

        我们将使用pathlib处理路径(Python 3 标准库的一部分),并使用requests下载数据集。 我们只会在使用模块时才导入它们,因此您可以确切地看到每个位置上正在使用的模块。

from pathlib import Path
import requests

DATA_PATH = Path("data")
PATH = DATA_PATH / "mnist"

PATH.mkdir(parents=True, exist_ok=True)

URL = "https://github.com/pytorch/tutorials/raw/master/_static/"
FILENAME = "mnist.pkl.gz"

if not (PATH / FILENAME).exists():
        content = requests.get(URL + FILENAME).content
        (PATH / FILENAME).open("wb").write(content)

该数据集为 numpy 数组格式,并已使用pickle(一种用于序列化数据的 python 特定格式)存储:

import pickle
import gzip

with gzip.open((PATH / FILENAME).as_posix(), "rb") as f:
        ((x_train, y_train), (x_valid, y_valid), _) = pickle.load(f, encoding="latin-1")

每个图像为28 x 28,并存储为长度为784 = 28x28的扁平行。 让我们来看一个; 我们需要先将其重塑为 2d:

from matplotlib import pyplot
import numpy as np

pyplot.imshow(x_train[0].reshape((28, 28)), cmap="gray")
print(x_train.shape)

PyTorch 使用torch.tensor而不是 numpy 数组,因此我们需要转换数据:

import torch

x_train, y_train, x_valid, y_valid = map(
    torch.tensor, (x_train, y_train, x_valid, y_valid)
)
n, c = x_train.shape
x_train, x_train.shape, y_train.min(), y_train.max()
print(x_train, y_train)
print(x_train.shape)
print(y_train.min(), y_train.max())

手搓神经网络

        首先,我们仅使用 PyTorch 张量操作创建模型。PyTorch 提供了创建随机或零填充张量的方法,我们将使用它们来为简单的线性模型创建权重和偏差。 这些只是常规张量,还有一个非常特殊的附加值:我们告诉 PyTorch 它们需要梯度。 这使 PyTorch 记录了在张量上完成的所有操作,因此它可以在反向传播时自动计算的梯度!

        对于权重,我们在初始化之后设置requires_grad,因为我们不希望该步骤包含在梯度中。 (请注意,PyTorch 中的尾随_表示该操作是原地执行的。)

我们在这里用 Xavier 初始化(通过乘以1 / sqrt(n))来初始化权重:

import math

weights = torch.randn(784, 10) / ,math.sqrt(784)
weights.requires_grad_()
bias = torch.zeros(10, requires_grad = True)

        由于 PyTorch 具有自动计算梯度的功能,我们可以将任何标准的 Python 函数(或可调用对象)用作模型! 因此,我们只需编写一个普通矩阵乘法和广播加法即可创建一个简单的线性模型。 我们还需要激活函数,因此我们将编写并使用log_softmax。 请记住:尽管 PyTorch 提供了许多预写的损失函数,激活函数等,但是您可以使用纯 Python 轻松编写自己的函数。 PyTorch 甚至会自动为您的函数创建快速 GPU 或向量化的 CPU 代码.

def log_softmax(x):
    return x - x.exp().sum(-1).log().unsqueeze(-1)

def model(xb):
    return log_softmax(xb @ weights + bias)

        在上面,@代表点积运算。 我们将对一批数据(在本例中为 64 张图像)调用函数。 这是一个正向传播。 请注意,由于我们从随机权重开始,因此在这一阶段,我们的预测不会比随机预测更好。

bs = 64 # batch size

xb = x_train[0:bs]   # a mini-batch from x
preds = model(xb)    # predictions
preds[0], preds.shape
print(preds[0], preds.shape)

        如您所见,preds张量不仅包含张量值,还包含梯度函数。 稍后我们将使用它进行反向传播。

让我们实现负对数可能性作为损失函数(同样,我们只能使用标准 Python):

def nll(input, target):
    return -input[range(target.shape[0]), target].mean()

loss_func = nll

 让我们使用随机模型来检查损失,以便我们稍后查看反向传播后是否可以改善我们的损失:

yb = y_train[0:bs]
print(loss_func(preds, yb))

我们还实现一个函数来计算模型的准确率。 对于每个预测,如果具有最大值的索引与目标值匹配,则该预测是正确的:

def accuracy(out, yb):
    preds = torch.argmax(out, dim=1)
    return (preds == yb).float().mean()

print(accuracy(preds, yb))

现在,我们可以运行一个训练循环。 对于每次迭代,我们将:

  • 选择一个小批量数据(大小为bs
  • 使用模型进行预测
  • 计算损失
  • loss.backward()更新模型的梯度,在这种情况下为weightsbias

        现在,我们使用这些梯度来更新权重和偏差。 我们在torch.no_grad()上下文管理器中执行此操作,因为我们不希望在下一步的梯度计算中记录这些操作.

        然后,将梯度设置为零,以便为下一个循环做好准备。 否则,我们的梯度会记录所有已发生操作的运行记录(loss.backward()将梯度添加到已存储的内容中,而不是替换它们

lr = 0.5  # learning rate
epochs = 2  # how many epochs to train for

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        start_i = i * bs
        end_i = start_i + bs
        xb = x_train[start_i:end_i]
        yb = y_train[start_i:end_i]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        with torch.no_grad():
            weights -= weights.grad * lr
            bias -= bias.grad * lr
            weights.grad.zero_()
            bias.grad.zero_()

        就是这样:我们完全从头开始创建并训练了一个最小的神经网络(在这种情况下,是逻辑回归,因为我们没有隐藏层)!

理解Torch的几个模块

  • torch.nn
    • Module:创建一个行为类似于函数的可调用对象,但也可以包含状态(例如神经网络层权重)。 它知道其中包含的 Parameter ,并且可以将其所有梯度归零,遍历它们以进行权重更新等
    • Parameter:张量的包装器,用于告知 Module 具有在反向传播期间需要更新的权重。 仅更新具有require_grad属性集的张量
    • functional:一个模块(通常按照惯例导入到 F 名称空间中),其中包含激活函数,损失函数等。 以及卷积和线性层等层的无状态版本
  • torch.optim:包含诸如 SGD 的优化程序,这些优化程序在后面的步骤
  • Dataset 中更新 Parameter 的权重。 具有 __len__ 和 __getitem__ 的对象,包括 Pytorch 提供的类,例如 TensorDataset
  • DataLoader:获取任何 Dataset 并创建一个迭代器,该迭代器返回批量数据。

使用torch.nn.functional

        现在,我们将重构代码,使其执行与以前相同的操作,只是我们将开始利用 PyTorch 的nn类使其更加简洁和灵活。 从这里开始的每一步,应该使我们的代码占一个或多个特点:更简洁、更易理解、和/或更灵活。

        第一步也是最简单的步骤,就是用torch.nn.functional(通常按照惯例将其导入到名称空间F中)替换我们的手写激活和损失函数,从而缩短代码长度。 该模块包含torch.nn库中的所有函数(而该库的其他部分包含类)。 除了广泛的损失和激活函数外,您还会在这里找到一些方便的函数来创建神经网络,例如池化函数。 (还有一些用于进行卷积,线性层等的函数,但是正如我们将看到的那样,通常可以使用库的其他部分来更好地处理这些函数。)

        如果您使用的是负对数自然损失和对数 softmax 激活,那么 Pytorch 会提供结合了两者的单一函数F.cross_entropy。 因此,我们甚至可以从模型中删除激活函数。

import torch.nn.functional as F

loss_func = F.cross_entropy

def model(xb):
    return xb @ weights + bias

使用nn.Module重构

        接下来,我们将使用nn.Modulenn.Parameter进行更清晰,更简洁的训练循环。 我们将nn.Module子类化(它本身是一个类并且能够跟踪状态)。 在这种情况下,我们要创建一个类,该类包含前进步骤的权重,偏置和方法。 nn.Module具有许多我们将要使用的属性和方法(例如.parameters().zero_grad())。

注意:

   nn.Module(大写M)是 PyTorch 的特定概念,并且是我们将经常使用的一类。 不要将nn.Module与模块(小写m)的 Python 概念混淆,该模块是可以导入的 Python 代码文件。

from torch import nn

class Mnist_Logistic(nn.Module):
    def __init__(self):
        super.__init__()
        self.weights = nn.Parameter(torch.randn(784, 10) / math.sqrt(784))
        self.bias = nn.Parameter(torch.zeros(10))

    def forward(self, xb):
        return xb @ self.weights + self.bias
    
model = Mnist_Logistic()

        以前,在我们的训练循环中,我们必须按名称更新每个参数的值,并手动将每个参数的梯度分别归零,如下所示:

with torch.no_grad():
    weights -= weights.grad * lr
    bias -= bias.grad * lr
    weights.grad.zero_()
    bias.grad.zero_()

        现在我们可以利用model.parameters()model.zero_grad()(它们都由 PyTorch 为nn.Module定义)来使这些步骤更简洁,并且更不会出现忘记某些参数的错误,尤其是当我们有一个更复杂的模型的时候:

with torch.no_grad():
    for p in model.parameters(): p-= p.grad * lr
    model.zero_grad()

我们将把小的训练循环包装在fit函数中,以便稍后再运行:

def fit():
    for epoch in range(epochs):
        for i in range((n - 1) // bs + 1):
            start_i = i * bs
            end_i = start_i + bs
            xb = x_train[start_i:end_i]
            yb = y_train[start_i:end_i]
            pred = model(xb)
            loss = loss_func(pred, yb)

            loss.backward()
            with torch.no_grad():
                for p in model.parameters():
                    p -= p.grad * lr
                model.zero_grad()

fit()

使用nn.Linear重构

        我们继续重构我们的代码。 代替手动定义和初始化self.weightsself.bias并计算xb  @ self.weights + self.bias,我们将对线性层使用 Pytorch 类nn.Linear,这将为我们完成所有工作。 Pytorch 具有许多类型的预定义层,可以大大简化我们的代码,并且通常也可以使其速度更快。

class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.lin = nn.Linear(784, 10)

    def forward(self, xb):
        return self.lin(xb)

使用optim重构

        Pytorch 还提供了一个包含各种优化算法的包torch.optim。 我们可以使用优化器中的step方法采取向前的步骤,而不是手动更新每个参数。

这将使我们替换之前的手动编码优化步骤:

# 之前的
with torch.no_grad():
    for p in model.parameters(): p -= p.grad * lr
    model.zero_grad()

# 使用optim
from torch import optim

opt.step()
opt.zero_grad()
def get_model():
    model = Mnist_Logistic()
    return model, optim.SGD(model.parameters(), lr=lr)

使用Dataset重构

        PyTorch 有一个抽象的Dataset类。 数据集可以是具有__len__函数(由 Python 的标准len函数调用)和具有__getitem__函数作为对其进行索引的一种方法。 本教程演示了一个不错的示例,该示例创建一个自定义FacialLandmarkDataset类作为Dataset的子类。

        PyTorch 的TensorDataset是一个数据集包装张量。 通过定义索引的长度和方式,这也为我们提供了沿张量的第一维进行迭代,索引和切片的方法。 这将使我们在训练的同一行中更容易访问自变量和因变量。

xb,yb = train_ds[i*bs : i*bs+bs]
model, opt = get_model()

for epoch in range(epochs):
    for i in range((n - 1) // bs + 1):
        xb, yb = train_ds[i * bs: i * bs + bs]
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

print(loss_func(model(xb), yb))

使用DataLoader重构

        Pytorch 的DataLoader负责批量管理。 您可以从任何Dataset创建一个DataLoader。 DataLoader使迭代迭代变得更加容易。 不必使用train_ds[i*bs : i*bs+bs]DataLoader会自动为我们提供每个小批量。

from torch.utils.data import DataLoader

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs)

添加验证

        在第 1 节中,我们只是试图建立一个合理的训练循环以用于我们的训练数据。 实际上,您也应该始终具有验证集,以便识别您是否过拟合

        对训练数据进行打乱对于防止批量与过拟合之间的相关性很重要。 另一方面,无论我们是否打乱验证集,验证损失都是相同的。 由于打乱需要花费更多时间,因此打乱验证数据没有任何意义。

        我们将验证集的批量大小设为训练集的两倍。 这是因为验证集不需要反向传播,因此占用的内存更少(不需要存储梯度)。 我们利用这一优势来使用更大的批量,并更快地计算损失。

train_ds = TensorDataset(x_train, y_train)
train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True)

valid_ds = TensorDataset(x_valid, y_valid)
valid_dl = DataLoader(valid_ds, batch_size=bs * 2)
# 我们将在每个周期结束时计算并打印验证损失

# 请注意,我们总是在训练之前调用model.train(),并在推理之前调用model.eval()
# 因为诸如nn.BatchNorm2d和nn.Dropout之类的层会使用它们,以确保这些不同阶段的行为正确

model, opt = get_model()

for epoch in range(epochs):
    model.train()
    for xb, yb in train_dl:
        pred = model(xb)
        loss = loss_func(pred, yb)

        loss.backward()
        opt.step()
        opt.zero_grad()

    model.eval()
    with torch.no_grad():
        valid_loss = sum(loss_func(model(xb), yb) for xb, yb in valid_dl)

    print(epoch, valid_loss / len(valid_dl))

创建fit( ) 和 get_data( )

        现在,我们将自己进行一些重构。 由于我们经历了两次相似的过程来计算训练集和验证集的损失,因此我们将其设为自己的函数loss_batch,该函数可计算一批损失。

        我们将优化器传入训练集中,然后使用它执行反向传播。 对于验证集,我们没有通过优化程序,因此该方法不会执行反向传播。

def loss_batch(model, loss_func, xb, yb, opt=None):
    loss = loss_func(model(xb), yb)

    if opt is not None:
        loss.backward()
        opt.step()
        opt.zero_grad()

    return loss.item(), len(xb)

fit运行必要的操作来训练我们的模型,并计算每个周期的训练和验证损失。

import numpy as np

def fit(epochs, model, loss_func, opt, train_dl, valid_dl):
    for epoch in range(epochs):
        model.train()
        for xb, yb in train_dl:
            loss_batch(model, loss_func, xb, yb, opt)

        model.eval()
        with torch.no_grad():
            losses, nums = zip(
                *[loss_batch(model, loss_func, xb, yb) for xb, yb in valid_dl]
            )
        val_loss = np.sum(np.multiply(losses, nums)) / np.sum(nums)

        print(epoch, val_loss)

get_data返回训练和验证集的数据加载器。

def get_data(train_ds, valid_ds, bs):
    return (
        DataLoader(train_ds, batch_size=bs, shuffle=True),
        DataLoader(valid_ds, batch_size=bs * 2),
    )

现在,我们获取数据加载器和拟合模型的整个过程可以在 3 行代码中运行:

train_dl, valid_dl = get_data(train_ds, valid_ds, bs)
model, opt = get_model()
fit(epochs, model, loss_func, opt, train_dl, valid_dl)

切换到CNN

        现在,我们将构建具有三个卷积层的神经网络。 由于上一节中的任何功能都不假设任何有关模型形式的信息,因此我们将能够使用它们来训练 CNN,而无需进行任何修改。

        我们将使用 Pytorch 的预定义Conv2d类作为我们的卷积层。 我们定义了具有 3 个卷积层的 CNN。 每个卷积后跟一个 ReLU。 最后,我们执行平均池化。 (请注意,view是 numpy 的reshape的 PyTorch 版本)

class Mnist_CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)
        self.conv2 = nn.Conv2d(16, 16, kernel_size=3, stride=2, padding=1)
        self.conv3 = nn.Conv2d(16, 10, kernel_size=3, stride=2, padding=1)

    def forward(self, xb):
        xb = xb.view(-1, 1, 28, 28)
        xb = F.relu(self.conv1(xb))
        xb = F.relu(self.conv2(xb))
        xb = F.relu(self.conv3(xb))
        xb = F.avg_pool2d(xb, 4)
        return xb.view(-1, xb.size(1))

lr = 0.1

动量是随机梯度下降的一种变体,它也考虑了以前的更新,通常可以加快训练速度:

model = Mnist_CNN()
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)

fit(epochs, model, loss_func, opt, train_dl, valid_dl)

一个实例:

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.utils.data import DataLoader
import torchvision.datasets as datasets
import torchvision.transforms as transforms

# Create FCNN


class NN(nn.Module):
    def __init__(self, input_size, num_classes):
        super(NN, self).__init__()
        self.fc1 = nn.Linear(input_size, 50)
        self.fc2 = nn.Linear(50, num_classes)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

    def count_parameters(self):
        return sum(p.numel() for p in self.parameters() if p.requires_grad) # list comprehension, works like a lambda

class CNN(nn.Module):
    def __init__(self, input_size=1, num_classes=10):
        super(CNN,self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1,out_channels=8, kernel_size=(3,3), stride=(1,1),padding=(1,1)) # keeps the dimension the same
        self.pool = nn.MaxPool2d(kernel_size=(2,2), stride=(2,2)) # 28 input size, with stride 2,2 we 1/4 the size of the layer( w/2 h/2)
        self.conv2 = nn.Conv2d(in_channels=8,out_channels=16,kernel_size=(3,3),stride=(1,1),padding=(1,1))
        self.fc1= nn.Linear(16*7*7, num_classes) # two pooling layers result in 28x28 --> 14x14 --> 7x7 layer size

    def forward(self,x):
        x=F.relu(self.conv1(x))
        x=self.pool(x)
        x=F.relu(self.conv2(x))
        x= self.pool(x)
        x=x.reshape(x.shape[0],-1)
        x=self.fc1(x)

        return x
    def count_parameters(self):
        return sum(p.numel() for p in self.parameters() if p.requires_grad)
# Set Device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print('running on {}'.format(device))
# Hyperparameters
in_size = 28*28
num_of_classes = 10
learning_rate = 0.001
batch_size = 64
num_epochs = 16

# Load Data
train_dataset = datasets.MNIST(
    root='dataset/', train=True, transform=transforms.ToTensor(), download=True)
train_loader = DataLoader(dataset=train_dataset,
                          batch_size=batch_size, shuffle=True)
test_dataset = datasets.MNIST(
    root='dataset/', train=False, transform=transforms.ToTensor(), download=True)
test_loader = DataLoader(dataset=test_dataset,
                         batch_size=batch_size, shuffle=True)

# initialize Network

model = CNN().to(device)
print('Number of Parameters: {}'.format(model.count_parameters()))
#Loss and Optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)


# Define the Accuracy Check Function

def check_accuracy(loader, model):
    if loader.dataset.train:
        print("Checking accuracy on training data")
    else:
        print("Checking accuracy on test data")

    num_correct = 0
    num_samples = 0
    model.eval()

    with torch.no_grad():
        for x, y in loader:
            x = x.to(device=device)
            y = y.to(device=device)

            scores = model(x)
            # 64 img x 10
            # the index of the max value is relevant not the max value ( a probability)
            _, predictions = scores.max(1)
            # of type tensor so we convert them for printing in floats
            num_correct += (predictions == y).sum()
            num_samples += predictions.size(0)
        acc = float(num_correct)/float(num_samples)*100
        print(f'Got {num_correct} / {num_samples} with accuracy {acc:.2f}')

    # model.train() # if you want to get the accuracy while training, we dont have a training method implemented yet, but trained beforehand
    return acc

# Train Network


for epoch in range(num_epochs):
    for batch_index, (data, targets) in enumerate(train_loader):
        # get data to cuda if possible
        data = data.to(device=device)
        targets = targets.to(device=device)
        # forward
        scores = model(data)
        loss = criterion(scores, targets)

        # backward
        optimizer.zero_grad()
        loss.backward()

        # gradient descent or adam step
        optimizer.step()
    print('epoch No: {}'.format(epoch))
    check_accuracy(train_loader, model)
    check_accuracy(test_loader, model)


# Check accuracy on training & test to see how good the model is


check_accuracy(train_loader, model)
check_accuracy(test_loader, model)

相关文章:

  • R语言进行数据分组聚合统计变换(Aggregating transforms)、计算dataframe数据的分组独特值的个数(distinct)
  • Linux入门之使用 ifconfig 命令配置网络连接
  • Day 1 BUUCTF——特殊的 BASE64 1
  • 大数据分析案例-用RFM模型对客户价值分析(聚类)
  • Linux入门之管理 Wi-Fi 连接
  • 结构体作业等
  • ARM架构种类
  • 【牛客刷题-算法】NC16 对称的二叉树
  • MATLAB算法实战应用案例精讲-【回归算法】XGBoost算法(附Java、Python和R语言代码)
  • 基于java春晓学堂管理系统计算机毕业设计源码+系统+lw文档+mysql数据库+调试部署
  • 通关率不到0.1%的小游戏《羊了个羊》为什么这么火?
  • [架构之路-7]:架构师 - 嵌入式硬件架构师的工作内容和工作要求是什么
  • 【javaweb简单教程】3.使用JDBC操作数据库(含简单示例完整代码)
  • 【uiautomation】微信群发消息,获取群通讯录名单
  • 「数据结构详解·八」带权并查集 扩展域并查集
  • [ 一起学React系列 -- 8 ] React中的文件上传
  • Apache的基本使用
  • co模块的前端实现
  • CSS盒模型深入
  • Effective Java 笔记(一)
  • JavaScript异步流程控制的前世今生
  • LeetCode算法系列_0891_子序列宽度之和
  • MySQL-事务管理(基础)
  • nodejs:开发并发布一个nodejs包
  • Rancher如何对接Ceph-RBD块存储
  • Redis字符串类型内部编码剖析
  • 复杂数据处理
  • 关于Java中分层中遇到的一些问题
  • 面试总结JavaScript篇
  • 我从编程教室毕业
  • 一个完整Java Web项目背后的密码
  • 移动端唤起键盘时取消position:fixed定位
  • ​【已解决】npm install​卡主不动的情况
  • ​LeetCode解法汇总1276. 不浪费原料的汉堡制作方案
  • #define 用法
  • (6)STL算法之转换
  • (AngularJS)Angular 控制器之间通信初探
  • (ISPRS,2023)深度语义-视觉对齐用于zero-shot遥感图像场景分类
  • (Repost) Getting Genode with TrustZone on the i.MX
  • (二)linux使用docker容器运行mysql
  • (附源码)spring boot基于小程序酒店疫情系统 毕业设计 091931
  • (每日持续更新)jdk api之StringBufferInputStream基础、应用、实战
  • (切换多语言)vantUI+vue-i18n进行国际化配置及新增没有的语言包
  • (亲测成功)在centos7.5上安装kvm,通过VNC远程连接并创建多台ubuntu虚拟机(ubuntu server版本)...
  • (入门自用)--C++--抽象类--多态原理--虚表--1020
  • (转)真正的中国天气api接口xml,json(求加精) ...
  • .net core 6 使用注解自动注入实例,无需构造注入 autowrite4net
  • .Net MVC + EF搭建学生管理系统
  • .NET 表达式计算:Expression Evaluator
  • .NET 服务 ServiceController
  • .NET 命令行参数包含应用程序路径吗?
  • .NET/C# 使用 SpanT 为字符串处理提升性能
  • .NET牛人应该知道些什么(2):中级.NET开发人员
  • .net中的Queue和Stack
  • :如何用SQL脚本保存存储过程返回的结果集