关于 0.1+0.2 == 0.3 不成立的一些细节

本文深入探讨了0.1+0.2!=0.3这一现象背后的原理,通过分析IEEE754标准下的浮点数存储格式及加法运算过程,解释了不同精度下浮点数比较的真伪。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

很早之前看到一关于js的问题,如下

实际上 0.1+0.2 != 0.3 这个问题不是js特有,来看一段java代码

    @Test
    public strictfp void test() {
        System.out.println(0.1f + 0.2f == 0.3f);
        System.out.println(0.1 + 0.2 == 0.3);
    }

程序的运行结果是

true
false


这个问题应该是基础中的基础,众所周知,这是一个不可避免的浮点数精度丢失问题,同十进制一样,二进制中也会存在无限循环小数,而计算机对浮点数的表示通常是用 32 或 64 位的二进制,这使得在二进制的表达上必须采用“截断”的手段丢掉一些二进制位

下面我来分析为什么上面的运行结果是这样,由于Java不能直接查看浮点数的十六进制表达,所以用c语言来分析

1、IEEE754 标准

IEEE754是最广泛运用的浮点数表达的格式。Java里面有一个不怎么常见的strictfp关键字,用于修饰方法,被修饰的方法中的浮点运算都会严格遵守IEEE754规范。关于IEE754的细节,可以翻阅相关资料,这里就讨论主要的。

2、IEEE754 标准下浮点数的存储格式

浮点数的存储分为三个部分,s, e, f,对应符号(sign),指数(exp),有效数位(fraction)

对于float,s+e+f = 1+8+23

对于double,s+e+f=1+11+52

来看下面一段代码

准备两个函数,用于显示浮点数的二进制表达

void float_hex(float* f) {
	int *p = (int *)f;
	printf("%f -> %08x\n", *f, *p);
}

void double_hex(double* d){
	int *p = (int *)d;
	printf("%lf -> %08x %08x\n", *d, *p, *(p+1));
}

main函数的调用

int main() {
	float f = 0.1f;
	float_hex(&f);
	
	double d = 0.1;
	double_hex(&d);
        return 0;
}
运行结果

3dcccccd 对应的二进制
0011 1101 1100 1100 1100 1100 1100 1101

1)规范化表达

下面来验证一下,对于0.1,转化成二进制表达

0.000011001100110011....

规范化表达(类似于十进制的科学记数法)

1.100110011001100 x 2^-4

其中符号位0。

2)指数的表达

接下来的指数部分应该是-4,可是实际上存储的是是0111 1011,实际上这是移码的表达,移的是127,也就是指数部分存储的最终内容是127+(-4)=123,也就是0111 1011

3)有效部分截断时的四舍五入

有效数位部分按道理来讲是 1100 1100 1100 的循环直至截断,由于规范化表达,使得小数点左边的一位一定是1,所以这一位被省略了,剩下的就是1001 1001 1001的循环,由于有效只能表达23位,所以应该是(灰色表达要被丢弃的部分)

100 1100 1100 1100 1100 1100 1100 1100...

细心的人会注意到最后,四位并不是1100,而是1101,这是因为截断尾数时采用了四舍五入的方式,如果截断的部分第一位为1,说明截断部分的数值大于或等于上一位的1/2,因此向前进一位所表达的误差会更小,因此被截断的 1100 1100 1100.. 会对上一位产生进位影响,所以最后四位是1100 + 1 = 1101

对于double型也是一样的方式,值得注意的是代码中显示的是 9999 9999a 3fb9 9999,

但实际上这个double的表达是 3fb9 9999 9999 9999a,代码打印的结果是因为这里采用大端存储的缘故


3、浮点数加法

指数不同的两个浮点数是不能直接相加的,拿

0.2的有效数位表达和0.1是相同的,但是指数部分比0.1的指数多1,因为恰好0.1x2=0.2,也可以写代码验证一下。

0.1的指数部分是-4,0.2指数部分是-3,小阶要向大阶“看齐”,0.1若要表示成指数为-3,就需要将有效数位整体右移,这样一来就能对有效数位相加

 1.100 1100 1100 1100 1100 1101 0

 0.110 0110 0110 0110 0110 0110  

______________________________________

10.011 0011 0011 0011 0011 0011 1

橙色部分不会因为右移而丢失,因为在计算浮点数的时候会运用两个比float位数多的临时变量来计算,double也是

规范化,四舍五入后尾数为

001 1001 1001 1001 1010

指数为-2,表达出来是 0111 1101,0.1+0.2的结果应该表达为

0011 1110 1001 1001 1001 1001 1001 1010(3E99 999A)

4、为什么 0.1f+0.2f == 0.3f 成立

上面分析的是0.1+0.2都是浮点数表达,即0.1f+0.2f,结果是3E99 999A,直接用浮点数表达0.3f,也会得到结果3E99 999A,所以Java代码中第一个双等号的结果是true。因此,这个 true 的产生是二者截断后恰好都有进位的原因,并不是精确意义上的相等。

5、为什么0.1+0.2 == 0.3 不成立

用同样的方式分析double,给double型变量直接赋值 0.3 和赋值 0.1+0.2 会得到不一样的结果,具体原因是 0.1 + 0.2的时候尾数截断产生了进位,而直接表达 0.3 的时候没有,详细流程可以自己分析一下,这里不赘述。

因此Java代码中第二个双等号的结果是false


6、判断浮点数的运算结果

如果要想判断0.1+0.2==0.3,应该写成形如 Math.abs(0.1+0.2 - 0.3) < 1e-6 的形式。

# 文件:train_swinunetr_earlyfusion.py import os import numpy as np import torch import torch.nn as nn from torch.utils.data import DataLoader from glob import glob from tqdm import tqdm from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt import matplotlib as mpl from torch.cuda.amp import GradScaler, autocast from torch.optim.lr_scheduler import ReduceLROnPlateau from monai.transforms import ( Compose, LoadImaged, EnsureChannelFirstd, Spacingd, Orientationd, ScaleIntensityRanged, RandCropByPosNegLabeld, RandFlipd, RandRotate90d, EnsureTyped, Resized, RandZoomd, RandGaussianNoised, CenterSpatialCropd, Activations, AsDiscrete ) from monai.data import PersistentDataset, list_data_collate, decollate_batch from monai.networks.nets import SwinUNETR from monai.metrics import DiceMetric from monai.losses import DiceCELoss mpl.rcParams[&#39;font.sans-serif&#39;] = [&#39;DejaVu Sans&#39;] mpl.rcParams[&#39;axes.unicode_minus&#39;] = False # ========================== 参数配置 ========================== root_dir = "datasets/LiTS/processed" images = sorted(glob(os.path.join(root_dir, "images", "*.nii.gz"))) labels = sorted(glob(os.path.join(root_dir, "labels", "*.nii.gz"))) data = [{"image": img, "label": lbl} for img, lbl in zip(images, labels)] train_files, val_files = train_test_split(data, test_size=0.2, random_state=42) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") max_epochs = 200 batch_size = 1 num_classes = 3 learning_rate = 1e-4 clip_dim = 512 use_amp = False base_size = (128, 128, 64) crop_size = tuple([max(32, s // 32 * 32) for s in (64, 64, 32)]) resized_size = tuple([max(32, s // 32 * 32) for s in base_size]) print(f"使用尺寸: resized={resized_size}, crop={crop_size}") # ===================== 数据预处理 ===================== train_transforms = Compose([ LoadImaged(keys=["image", "label"]), EnsureChannelFirstd(keys=["image", "label"]), Orientationd(keys=["image", "label"], axcodes="RAS"), Spacingd(keys=["image", "label"], pixdim=(1.5, 1.5, 2.0), mode=("bilinear", "nearest")), ScaleIntensityRanged(keys=["image"], a_min=-200, a_max=200, b_min=0.0, b_max=1.0, clip=True), Resized(keys=["image", "label"], spatial_size=resized_size, mode=("trilinear", "nearest")), RandCropByPosNegLabeld(keys=["image", "label"], label_key="label", spatial_size=crop_size, pos=1.0, neg=1.0, num_samples=1), RandFlipd(keys=["image", "label"], prob=0.5, spatial_axis=0), RandFlipd(keys=["image", "label"], prob=0.5, spatial_axis=1), RandFlipd(keys=["image", "label"], prob=0.5, spatial_axis=2), RandRotate90d(keys=["image", "label"], prob=0.5, max_k=3), RandZoomd(keys=["image", "label"], prob=0.5, min_zoom=0.8, max_zoom=1.2, mode=("trilinear", "nearest")), RandGaussianNoised(keys=["image"], prob=0.2, mean=0.0, std=0.1), EnsureTyped(keys=["image", "label"]), ]) val_transforms = Compose([ LoadImaged(keys=["image", "label"]), EnsureChannelFirstd(keys=["image", "label"]), Orientationd(keys=["image", "label"], axcodes="RAS"), Spacingd(keys=["image", "label"], pixdim=(1.5, 1.5, 2.0), mode=("bilinear", "nearest")), ScaleIntensityRanged(keys=["image"], a_min=-200, a_max=200, b_min=0.0, b_max=1.0, clip=True), Resized(keys=["image", "label"], spatial_size=resized_size, mode=("trilinear", "nearest")), CenterSpatialCropd(keys=["image", "label"], roi_size=crop_size), EnsureTyped(keys=["image", "label"]), ]) train_ds = PersistentDataset(train_files, transform=train_transforms, cache_dir="./cache/train") val_ds = PersistentDataset(val_files, transform=val_transforms, cache_dir="./cache/val") train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, collate_fn=list_data_collate, num_workers=4, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=1, shuffle=False, collate_fn=list_data_collate, num_workers=2, pin_memory=True) # =============== 加载文本特征 =============== clip_feats = np.load("./clip_text_features.npy") # shape: (N, 512) clip_feats = torch.from_numpy(clip_feats).float() def get_text_features(bs): idx = torch.randint(0, len(clip_feats), (bs,)) return clip_feats[idx].to(device) # =============== 融合模块定义 =============== class EarlyFusionCrossAttention(nn.Module): def __init__(self, img_dim=192, text_dim=512): super().__init__() self.query = nn.Linear(img_dim, img_dim) self.key = nn.Linear(text_dim, img_dim) self.value = nn.Linear(text_dim, img_dim) self.out = nn.Linear(img_dim, img_dim) def forward(self, img_feat, text_feat): B, C, D, H, W = img_feat.shape N = D * H * W img_flat = img_feat.view(B, C, N).permute(0, 2, 1) # (B, N, C) Q = self.query(img_flat) K = self.key(text_feat).unsqueeze(1) V = self.value(text_feat).unsqueeze(1) attn = torch.softmax(Q @ K.transpose(-2, -1) / C ** 0.5, dim=-1) out = attn @ V # (B, N, C) out = self.out(out).permute(0, 2, 1).view(B, C, D, H, W) return img_feat + out # =============== 主模型定义 (修复部分) =============== class SwinUNETRWithEarlyFusion(SwinUNETR): def __init__(self, img_size, in_channels, out_channels, feature_size=12, text_feat_dim=512): super().__init__( img_size=img_size, in_channels=in_channels, out_channels=out_channels, feature_size=feature_size ) # 添加融合模块 self.fusion = EarlyFusionCrossAttention( img_dim=feature_size * 16, # 匹配最后一层特征图的通道数 text_dim=text_feat_dim ) def forward(self, x, text_feat=None): # 获取编码器输出 enc_out = self.swinViT(x) # 返回列表 [x0, x1, x2, x3, x4] # 对最后一层特征进行融合 if text_feat is not None: enc_out[-1] = self.fusion(enc_out[-1], text_feat) # 使用解码器处理融合后的特征 return self.forward_up(enc_out) # 使用父类的forward_up方法 # =============== 模型训练相关 =============== model = SwinUNETRWithEarlyFusion( img_size=crop_size, in_channels=1, out_channels=num_classes, feature_size=12, text_feat_dim=clip_dim ).to(device) loss_fn = DiceCELoss(to_onehot_y=True, softmax=True, include_background=True, weight=torch.tensor([0.2, 0.3, 0.5]).to(device)) optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=1e-5) scheduler = ReduceLROnPlateau(optimizer, mode="max", patience=5, factor=0.5, verbose=True, min_lr=1e-6) post_pred = Compose([Activations(softmax=True), AsDiscrete(argmax=True, to_onehot=num_classes)]) post_label = Compose([AsDiscrete(to_onehot=num_classes)]) dice_metric = DiceMetric(include_background=True, reduction="mean", get_not_nans=False, num_classes=num_classes) scaler = GradScaler(enabled=use_amp) best_dice = -1 os.makedirs("earlyfusion_checkpoints", exist_ok=True) # =============== 训练循环 =============== for epoch in range(1, max_epochs + 1): print(f"\nEpoch {epoch}/{max_epochs}") model.train() epoch_loss = 0 for batch in tqdm(train_loader, desc="Train"): images = batch["image"].to(device) labels = batch["label"].to(device) text_feat = get_text_features(images.shape[0]) optimizer.zero_grad() with autocast(enabled=use_amp): outputs = model(images, text_feat) loss = loss_fn(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() epoch_loss += loss.item() print(f"Train Loss: {epoch_loss / len(train_loader):.4f}") # 验证 model.eval() val_dices = [] with torch.no_grad(): for batch in tqdm(val_loader, desc="Val"): images = batch["image"].to(device) labels = batch["label"].to(device) text_feat = get_text_features(images.shape[0]) with autocast(enabled=use_amp): outputs = model(images, text_feat) outputs_list = decollate_batch(outputs) labels_list = decollate_batch(labels) outputs_convert = [post_pred(o) for o in outputs_list] labels_convert = [post_label(l) for l in labels_list] dice_metric(y_pred=outputs_convert, y=labels_convert) val_dices.append(dice_metric.aggregate().item()) dice_metric.reset() avg_dice = np.mean(val_dices) print(f"Val Dice: {avg_dice:.4f}") scheduler.step(avg_dice) # 保存最优模型 if avg_dice > best_dice: best_dice = avg_dice torch.save(model.state_dict(), f"earlyfusion_checkpoints/best_model_epoch{epoch}_dice{avg_dice:.4f}.pth") print(f"✅ Saved best model at epoch {epoch} with Dice {avg_dice:.4f}")这个代码报错啦 return self.forward_up(enc_out) # 使用父类的forward_up方法 File "/home/liulicheng/anaconda3/envs/covid_seg/lib/python3.8/site-packages/torch/nn/modules/module.py", line 1695, in __getattr__ raise AttributeError(f"&#39;{type(self).__name__}&#39; object has no attribute &#39;{name}&#39;") AttributeError: &#39;SwinUNETRWithEarlyFusion&#39; object has no attribute &#39;forward_up&#39;
07-01
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值